feat: show bd leader invites in modal
This commit is contained in:
parent
e1abcb0b4e
commit
850b340600
@ -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-2130" />
|
||||
<link rel="stylesheet" href="./style.css?v=20260428-2145" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="bd-leader-center" aria-label="BD Leader Center" data-i18n-aria="page_label" data-loading="true">
|
||||
@ -116,21 +116,21 @@
|
||||
</span>
|
||||
<span class="chevron">›</span>
|
||||
</button>
|
||||
<button class="menu-row" type="button" data-route="/invite-agency">
|
||||
<button class="menu-row" type="button" data-action="invite-agent">
|
||||
<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 data-i18n="invite_become_agent">Invite Agent</span>
|
||||
</span>
|
||||
<span class="chevron">›</span>
|
||||
</button>
|
||||
<button class="menu-row" type="button" data-route="/invite-bd">
|
||||
<button class="menu-row" type="button" data-action="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 data-i18n="invite_become_bd">Invite BD</span>
|
||||
</span>
|
||||
<span class="chevron">›</span>
|
||||
</button>
|
||||
@ -183,6 +183,30 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="invite-modal" id="inviteModal" hidden>
|
||||
<div class="invite-backdrop" data-close-invite></div>
|
||||
<section class="invite-dialog" role="dialog" aria-modal="true" aria-labelledby="inviteModalTitle">
|
||||
<div class="invite-head">
|
||||
<h2 id="inviteModalTitle">Invite BD</h2>
|
||||
<button class="invite-close" type="button" aria-label="Close" data-i18n-aria="close" data-close-invite>×</button>
|
||||
</div>
|
||||
<div class="invite-body">
|
||||
<form class="invite-search-form" id="inviteSearchForm" autocomplete="off">
|
||||
<label class="invite-field">
|
||||
<span data-i18n="enter_user_id">Enter User ID</span>
|
||||
<div class="invite-search-control">
|
||||
<input id="inviteSearchInput" type="text" inputmode="text" data-i18n-placeholder="enter_user_id" placeholder="Enter User ID" />
|
||||
<button id="inviteSearchButton" type="submit" data-i18n="search">Search</button>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
<div class="invite-status" id="inviteStatus" role="status" hidden></div>
|
||||
<div class="invite-list" id="inviteList"></div>
|
||||
<div class="invite-empty" id="inviteEmpty" data-i18n="no_users_found">No users found</div>
|
||||
</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">
|
||||
@ -199,6 +223,6 @@
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
<script src="./script.js?v=20260428-2130" defer></script>
|
||||
<script src="./script.js?v=20260428-2145" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -25,8 +25,17 @@
|
||||
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",
|
||||
invite_become_agent: "Invite Agent",
|
||||
invite_become_bd: "Invite BD",
|
||||
invite_agent_title: "Invite Agent",
|
||||
invite_bd_title: "Invite BD",
|
||||
enter_user_id: "Enter User ID",
|
||||
search: "Search",
|
||||
searching: "Searching...",
|
||||
invite: "Invite",
|
||||
inviting: "Inviting...",
|
||||
cancel: "Cancel",
|
||||
success: "Success",
|
||||
bd_list_preview: "BD List",
|
||||
bd_list_preview_subtitle: "Current settlement period",
|
||||
more: "More",
|
||||
@ -40,6 +49,12 @@
|
||||
pending: "Pending",
|
||||
completed: "Completed",
|
||||
out_of_account: "Out of account",
|
||||
no_users_found: "No users found",
|
||||
invitation_submitted: "Invitation submitted",
|
||||
application_cancelled: "Application cancelled",
|
||||
something_went_wrong: "Something went wrong",
|
||||
failed_to_load_invites: "Failed to load invitations",
|
||||
cancellation_failed_try_again: "Cancellation failed. Try again.",
|
||||
close: "Close",
|
||||
failed_to_load: "Failed to load data",
|
||||
failed_to_load_history: "Failed to load history"
|
||||
@ -60,8 +75,17 @@
|
||||
bd_leader_history: "سجل قائد BD",
|
||||
agency_list_link: "قائمة الوكالات",
|
||||
bd_list_link: "قائمة BD",
|
||||
invite_become_agent: "دعوة ليصبح وكيلا",
|
||||
invite_become_bd: "دعوة ليصبح BD",
|
||||
invite_become_agent: "دعوة وكيل",
|
||||
invite_become_bd: "دعوة BD",
|
||||
invite_agent_title: "دعوة وكيل",
|
||||
invite_bd_title: "دعوة BD",
|
||||
enter_user_id: "أدخل معرف المستخدم",
|
||||
search: "بحث",
|
||||
searching: "جار البحث...",
|
||||
invite: "دعوة",
|
||||
inviting: "جار الدعوة...",
|
||||
cancel: "إلغاء",
|
||||
success: "نجاح",
|
||||
bd_list_preview: "قائمة BD",
|
||||
bd_list_preview_subtitle: "فترة التسوية الحالية",
|
||||
more: "المزيد",
|
||||
@ -75,6 +99,12 @@
|
||||
pending: "قيد الانتظار",
|
||||
completed: "مكتمل",
|
||||
out_of_account: "خارج الحساب",
|
||||
no_users_found: "لم يتم العثور على مستخدمين",
|
||||
invitation_submitted: "تم إرسال الدعوة",
|
||||
application_cancelled: "تم إلغاء الطلب",
|
||||
something_went_wrong: "حدث خطأ ما",
|
||||
failed_to_load_invites: "فشل تحميل الدعوات",
|
||||
cancellation_failed_try_again: "فشل الإلغاء. حاول مرة أخرى.",
|
||||
close: "إغلاق",
|
||||
failed_to_load: "فشل تحميل البيانات",
|
||||
failed_to_load_history: "فشل تحميل السجل"
|
||||
@ -95,8 +125,17 @@
|
||||
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",
|
||||
invite_become_agent: "Ajans Davet Et",
|
||||
invite_become_bd: "BD Davet Et",
|
||||
invite_agent_title: "Ajans Davet Et",
|
||||
invite_bd_title: "BD Davet Et",
|
||||
enter_user_id: "Kullanıcı ID gir",
|
||||
search: "Ara",
|
||||
searching: "Aranıyor...",
|
||||
invite: "Davet et",
|
||||
inviting: "Davet ediliyor...",
|
||||
cancel: "İptal",
|
||||
success: "Başarılı",
|
||||
bd_list_preview: "BD Listesi",
|
||||
bd_list_preview_subtitle: "Mevcut ödeme dönemi",
|
||||
more: "Daha fazla",
|
||||
@ -110,6 +149,12 @@
|
||||
pending: "Beklemede",
|
||||
completed: "Tamamlandı",
|
||||
out_of_account: "Hesap dışı",
|
||||
no_users_found: "Kullanıcı bulunamadı",
|
||||
invitation_submitted: "Davet gönderildi",
|
||||
application_cancelled: "Başvuru iptal edildi",
|
||||
something_went_wrong: "Bir şeyler ters gitti",
|
||||
failed_to_load_invites: "Davetler yüklenemedi",
|
||||
cancellation_failed_try_again: "İptal başarısız. Tekrar deneyin.",
|
||||
close: "Kapat",
|
||||
failed_to_load: "Veri yüklenemedi",
|
||||
failed_to_load_history: "Geçmiş yüklenemedi"
|
||||
@ -130,8 +175,17 @@
|
||||
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",
|
||||
invite_become_agent: "Invite Agent",
|
||||
invite_become_bd: "Invite BD",
|
||||
invite_agent_title: "Invite Agent",
|
||||
invite_bd_title: "Invite BD",
|
||||
enter_user_id: "Masukkan ID Pengguna",
|
||||
search: "Cari",
|
||||
searching: "Mencari...",
|
||||
invite: "Undang",
|
||||
inviting: "Mengundang...",
|
||||
cancel: "Batal",
|
||||
success: "Berhasil",
|
||||
bd_list_preview: "Daftar BD",
|
||||
bd_list_preview_subtitle: "Periode settlement saat ini",
|
||||
more: "Lainnya",
|
||||
@ -145,6 +199,12 @@
|
||||
pending: "Menunggu",
|
||||
completed: "Selesai",
|
||||
out_of_account: "Di luar akun",
|
||||
no_users_found: "Pengguna tidak ditemukan",
|
||||
invitation_submitted: "Undangan terkirim",
|
||||
application_cancelled: "Pengajuan dibatalkan",
|
||||
something_went_wrong: "Terjadi kesalahan",
|
||||
failed_to_load_invites: "Gagal memuat undangan",
|
||||
cancellation_failed_try_again: "Gagal membatalkan. Coba lagi.",
|
||||
close: "Tutup",
|
||||
failed_to_load: "Gagal memuat data",
|
||||
failed_to_load_history: "Gagal memuat riwayat"
|
||||
@ -161,7 +221,18 @@
|
||||
balanceTotal: {},
|
||||
bill: {},
|
||||
history: null,
|
||||
historyMore: null
|
||||
historyMore: null,
|
||||
invite: {
|
||||
mode: "agent",
|
||||
invited: [],
|
||||
searched: [],
|
||||
showSearch: false,
|
||||
query: "",
|
||||
locked: false,
|
||||
searching: false,
|
||||
statusText: "",
|
||||
statusType: ""
|
||||
}
|
||||
};
|
||||
|
||||
function readURLParams() {
|
||||
@ -223,6 +294,11 @@
|
||||
const value = messages[node.dataset.i18nAria] || fallbackMessages.en[node.dataset.i18nAria];
|
||||
if (value) node.setAttribute("aria-label", value);
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => {
|
||||
const value = messages[node.dataset.i18nPlaceholder] || fallbackMessages.en[node.dataset.i18nPlaceholder];
|
||||
if (value) node.setAttribute("placeholder", value);
|
||||
});
|
||||
}
|
||||
|
||||
function apiBaseURL() {
|
||||
@ -249,14 +325,23 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function requestJSON(path) {
|
||||
async function requestJSON(path, options = {}) {
|
||||
const token = readRawParam("token");
|
||||
if (!token) throw new Error("Missing token");
|
||||
|
||||
const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), {
|
||||
const headers = authHeadersFromToken(token);
|
||||
const request = {
|
||||
method: options.method || "GET",
|
||||
cache: "no-store",
|
||||
headers: authHeadersFromToken(token)
|
||||
});
|
||||
headers
|
||||
};
|
||||
|
||||
if (options.body !== undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
request.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), request);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok || data.status === false) {
|
||||
const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
|
||||
@ -275,7 +360,7 @@
|
||||
}
|
||||
|
||||
function updateModalLock() {
|
||||
const hasOpen = Array.from(document.querySelectorAll(".history-modal, .details-modal")).some((modal) => !modal.hidden);
|
||||
const hasOpen = Array.from(document.querySelectorAll(".history-modal, .details-modal, .invite-modal")).some((modal) => !modal.hidden);
|
||||
document.body.classList.toggle("modal-open", hasOpen);
|
||||
}
|
||||
|
||||
@ -591,6 +676,335 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function inviteConfig(mode = state.invite.mode) {
|
||||
if (mode === "bd") {
|
||||
return {
|
||||
titleKey: "invite_bd_title",
|
||||
list: "/team/bd/invite-bd-message-own",
|
||||
invite: "/team/bd/invite-bd",
|
||||
cancel: (id) => `/team/bd/invite-bd-cancel/${encodeURIComponent(id)}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
titleKey: "invite_agent_title",
|
||||
list: "/team/bd/invite-message-own",
|
||||
invite: "/team/bd/invite-agent",
|
||||
cancel: (id) => `/team/bd/invite-agent-cancel/${encodeURIComponent(id)}`
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeInviteMode(mode) {
|
||||
return mode === "bd" ? "bd" : "agent";
|
||||
}
|
||||
|
||||
function inviteItemProfile(item) {
|
||||
return item?.userProfile || item?.profile || item?.inviteUserProfile || item || {};
|
||||
}
|
||||
|
||||
function inviteProfileId(profile) {
|
||||
return String(profile?.id || profile?.userId || "");
|
||||
}
|
||||
|
||||
function sameInviteProfile(left, right) {
|
||||
const leftId = inviteProfileId(left);
|
||||
const rightId = inviteProfileId(right);
|
||||
if (leftId && rightId) return leftId === rightId;
|
||||
const leftAccount = String(left?.account || pickSpecialAccount(left) || "");
|
||||
const rightAccount = String(right?.account || pickSpecialAccount(right) || "");
|
||||
return Boolean(leftAccount && rightAccount && leftAccount === rightAccount);
|
||||
}
|
||||
|
||||
function inviteStatusType(status) {
|
||||
const value = String(status ?? "").toUpperCase();
|
||||
if (status === 0 || value === "0" || value === "PENDING") return "pending";
|
||||
if (status === 1 || value === "1" || value === "SUCCESS" || value === "ACCEPTED") return "success";
|
||||
return "";
|
||||
}
|
||||
|
||||
function mergeInviteStatus(profile) {
|
||||
const record = state.invite.invited.find((item) => sameInviteProfile(inviteItemProfile(item), profile));
|
||||
if (!record) return { userProfile: profile };
|
||||
return {
|
||||
...record,
|
||||
userProfile: {
|
||||
...profile,
|
||||
...inviteItemProfile(record)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setInviteStatus(text = "", type = "") {
|
||||
state.invite.statusText = text;
|
||||
state.invite.statusType = type;
|
||||
const status = document.querySelector("#inviteStatus");
|
||||
if (!status) return;
|
||||
status.textContent = text;
|
||||
status.hidden = !text;
|
||||
status.classList.toggle("is-error", type === "error");
|
||||
status.classList.toggle("is-success", type === "success");
|
||||
}
|
||||
|
||||
function createInviteFallback(name) {
|
||||
const span = document.createElement("span");
|
||||
span.className = "invite-avatar-fallback";
|
||||
span.textContent = String(name || "B").slice(0, 1).toUpperCase() || "B";
|
||||
return span;
|
||||
}
|
||||
|
||||
function createInviteAvatar(profile) {
|
||||
const avatar = document.createElement("div");
|
||||
avatar.className = "invite-user-avatar";
|
||||
const name = pickProfileName(profile);
|
||||
const avatarUrl = profile?.userAvatar || profile?.avatar || "";
|
||||
if (!avatarUrl) {
|
||||
avatar.appendChild(createInviteFallback(name));
|
||||
return avatar;
|
||||
}
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = avatarUrl;
|
||||
image.alt = "";
|
||||
image.addEventListener("error", () => {
|
||||
avatar.replaceChildren(createInviteFallback(name));
|
||||
});
|
||||
avatar.appendChild(image);
|
||||
return avatar;
|
||||
}
|
||||
|
||||
function createInviteCard(item) {
|
||||
const profile = inviteItemProfile(item);
|
||||
const status = inviteStatusType(item?.status);
|
||||
const card = document.createElement("article");
|
||||
card.className = "invite-user-card";
|
||||
|
||||
const main = document.createElement("div");
|
||||
main.className = "invite-user-main";
|
||||
const nameNode = document.createElement("div");
|
||||
nameNode.className = "invite-user-name";
|
||||
nameNode.textContent = pickProfileName(profile);
|
||||
const metaNode = document.createElement("div");
|
||||
metaNode.className = "invite-user-meta";
|
||||
metaNode.textContent = `${message("uid_prefix")} ${pickProfileAccount(profile)}`.trim();
|
||||
main.append(nameNode, metaNode);
|
||||
|
||||
const side = document.createElement("div");
|
||||
side.className = "invite-user-side";
|
||||
if (status === "pending") {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "invite-badge pending";
|
||||
badge.textContent = message("pending");
|
||||
const cancel = document.createElement("button");
|
||||
cancel.className = "invite-cancel";
|
||||
cancel.type = "button";
|
||||
cancel.textContent = message("cancel");
|
||||
cancel.disabled = state.invite.locked;
|
||||
cancel.addEventListener("click", () => cancelInvite(item?.id));
|
||||
side.append(badge, cancel);
|
||||
} else if (status === "success") {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "invite-badge success";
|
||||
badge.textContent = message("success");
|
||||
side.appendChild(badge);
|
||||
} else {
|
||||
const invite = document.createElement("button");
|
||||
invite.className = "invite-action";
|
||||
invite.type = "button";
|
||||
invite.textContent = state.invite.locked ? message("inviting") : message("invite");
|
||||
invite.disabled = state.invite.locked || !inviteProfileId(profile);
|
||||
invite.addEventListener("click", () => inviteUser(profile));
|
||||
side.appendChild(invite);
|
||||
}
|
||||
|
||||
card.append(createInviteAvatar(profile), main, side);
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderInviteModal() {
|
||||
const config = inviteConfig();
|
||||
const title = document.querySelector("#inviteModalTitle");
|
||||
const input = document.querySelector("#inviteSearchInput");
|
||||
const searchButton = document.querySelector("#inviteSearchButton");
|
||||
const list = document.querySelector("#inviteList");
|
||||
const empty = document.querySelector("#inviteEmpty");
|
||||
const rows = state.invite.showSearch ? state.invite.searched : state.invite.invited;
|
||||
|
||||
if (title) title.textContent = message(config.titleKey);
|
||||
if (input && document.activeElement !== input) input.value = state.invite.query;
|
||||
if (searchButton) {
|
||||
searchButton.disabled = state.invite.locked;
|
||||
searchButton.textContent = state.invite.searching ? message("searching") : message("search");
|
||||
}
|
||||
setInviteStatus(state.invite.statusText, state.invite.statusType);
|
||||
if (list) replaceChildren(list, rows.map(createInviteCard));
|
||||
if (empty) empty.hidden = rows.length > 0 || Boolean(state.invite.statusText);
|
||||
}
|
||||
|
||||
async function loadInviteRecords(options = {}) {
|
||||
const config = inviteConfig();
|
||||
if (!options.silent) setInviteStatus(message("loading_data"));
|
||||
renderInviteModal();
|
||||
try {
|
||||
const records = await requestJSON(config.list);
|
||||
state.invite.invited = Array.isArray(records) ? records : [];
|
||||
if (!options.preserveStatus) setInviteStatus("");
|
||||
} catch (error) {
|
||||
console.error("Failed to load invite records:", error);
|
||||
setInviteStatus(error.response?.errorMsg || error.message || message("failed_to_load_invites"), "error");
|
||||
} finally {
|
||||
renderInviteModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function searchInviteProfile(query) {
|
||||
let profile = null;
|
||||
let firstError = null;
|
||||
try {
|
||||
profile = await requestJSON(`/user/user-profile/open-search?account=${encodeURIComponent(query)}`);
|
||||
} catch (error) {
|
||||
firstError = error;
|
||||
}
|
||||
|
||||
if (!profile && /^\d+$/.test(query)) {
|
||||
try {
|
||||
profile = await requestJSON(`/user/user-profile?userId=${encodeURIComponent(query)}`);
|
||||
firstError = null;
|
||||
} catch (error) {
|
||||
if (!firstError) firstError = error;
|
||||
}
|
||||
}
|
||||
|
||||
const primaryProfile = Array.isArray(profile) ? profile[0] : profile;
|
||||
const normalizedProfile = inviteItemProfile(primaryProfile);
|
||||
if (normalizedProfile && (!pickProfileName(normalizedProfile) || pickProfileName(normalizedProfile) === "-" || !pickSpecialAccount(normalizedProfile) || !normalizedProfile.userAvatar)) {
|
||||
const userId = inviteProfileId(normalizedProfile) || (/^\d+$/.test(query) ? query : "");
|
||||
if (userId) {
|
||||
try {
|
||||
const detail = await requestJSON(`/user/user-profile?userId=${encodeURIComponent(userId)}`);
|
||||
const mergedProfile = { ...normalizedProfile, ...inviteItemProfile(detail) };
|
||||
profile = Array.isArray(profile)
|
||||
? [{ ...primaryProfile, userProfile: mergedProfile }, ...profile.slice(1)]
|
||||
: (primaryProfile?.userProfile ? { ...primaryProfile, userProfile: mergedProfile } : mergedProfile);
|
||||
} catch (error) {
|
||||
console.debug("Invite search detail fallback skipped:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!profile && firstError) throw firstError;
|
||||
return Array.isArray(profile) ? profile : (profile ? [profile] : []);
|
||||
}
|
||||
|
||||
async function searchInviteUser(event) {
|
||||
if (event) event.preventDefault();
|
||||
if (state.invite.locked) return;
|
||||
|
||||
const input = document.querySelector("#inviteSearchInput");
|
||||
const query = String(input?.value || "").trim();
|
||||
state.invite.query = query;
|
||||
state.invite.showSearch = Boolean(query);
|
||||
state.invite.searched = [];
|
||||
if (!query) {
|
||||
setInviteStatus("");
|
||||
renderInviteModal();
|
||||
return;
|
||||
}
|
||||
|
||||
state.invite.locked = true;
|
||||
state.invite.searching = true;
|
||||
setInviteStatus(message("searching"));
|
||||
renderInviteModal();
|
||||
try {
|
||||
const profiles = await searchInviteProfile(query);
|
||||
state.invite.searched = profiles
|
||||
.map((profile) => inviteItemProfile(profile))
|
||||
.filter((profile) => inviteProfileId(profile) || profile?.account)
|
||||
.map(mergeInviteStatus);
|
||||
setInviteStatus("");
|
||||
} catch (error) {
|
||||
console.error("Invite search failed:", error);
|
||||
setInviteStatus(error.response?.errorMsg || error.message || message("something_went_wrong"), "error");
|
||||
} finally {
|
||||
state.invite.locked = false;
|
||||
state.invite.searching = false;
|
||||
renderInviteModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function inviteUser(profile) {
|
||||
const userId = inviteProfileId(profile);
|
||||
if (state.invite.locked || !userId) return;
|
||||
|
||||
const config = inviteConfig();
|
||||
state.invite.locked = true;
|
||||
renderInviteModal();
|
||||
try {
|
||||
await requestJSON(config.invite, {
|
||||
method: "POST",
|
||||
body: { inviteUserId: userId }
|
||||
});
|
||||
showToast(message("invitation_submitted"));
|
||||
state.invite.query = "";
|
||||
state.invite.showSearch = false;
|
||||
state.invite.searched = [];
|
||||
const input = document.querySelector("#inviteSearchInput");
|
||||
if (input) input.value = "";
|
||||
await loadInviteRecords({ silent: true });
|
||||
} catch (error) {
|
||||
console.error("Invite request failed:", error);
|
||||
setInviteStatus(error.response?.errorMsg || error.message || message("something_went_wrong"), "error");
|
||||
} finally {
|
||||
state.invite.locked = false;
|
||||
renderInviteModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelInvite(id) {
|
||||
if (state.invite.locked || !id) return;
|
||||
|
||||
const config = inviteConfig();
|
||||
state.invite.locked = true;
|
||||
renderInviteModal();
|
||||
try {
|
||||
await requestJSON(config.cancel(id), { method: "POST" });
|
||||
showToast(message("application_cancelled"));
|
||||
state.invite.query = "";
|
||||
state.invite.showSearch = false;
|
||||
state.invite.searched = [];
|
||||
const input = document.querySelector("#inviteSearchInput");
|
||||
if (input) input.value = "";
|
||||
await loadInviteRecords({ silent: true });
|
||||
} catch (error) {
|
||||
console.error("Cancel invite failed:", error);
|
||||
const text = error.response?.errorMsg?.replace(/^.*\]\s*/, "") || message("cancellation_failed_try_again");
|
||||
setInviteStatus(text, "error");
|
||||
} finally {
|
||||
state.invite.locked = false;
|
||||
renderInviteModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function showInviteModal(mode) {
|
||||
state.invite.mode = normalizeInviteMode(mode);
|
||||
state.invite.invited = [];
|
||||
state.invite.searched = [];
|
||||
state.invite.showSearch = false;
|
||||
state.invite.query = "";
|
||||
state.invite.locked = false;
|
||||
state.invite.searching = false;
|
||||
setInviteStatus("");
|
||||
const input = document.querySelector("#inviteSearchInput");
|
||||
if (input) input.value = "";
|
||||
renderInviteModal();
|
||||
setModalOpen("#inviteModal", true);
|
||||
window.setTimeout(() => document.querySelector("#inviteSearchInput")?.focus(), 80);
|
||||
await loadInviteRecords();
|
||||
}
|
||||
|
||||
function closeInviteModal() {
|
||||
setModalOpen("#inviteModal", false);
|
||||
}
|
||||
|
||||
async function fetchProfile() {
|
||||
state.profile = await requestJSON("/team/member/profile");
|
||||
renderProfile();
|
||||
@ -719,6 +1133,7 @@
|
||||
renderBill();
|
||||
renderHistory();
|
||||
renderDetails();
|
||||
renderInviteModal();
|
||||
}
|
||||
|
||||
function clearPayee() {
|
||||
@ -757,16 +1172,23 @@
|
||||
node.addEventListener("click", () => navigateTo(node.dataset.route));
|
||||
});
|
||||
document.querySelector("[data-action='history']")?.addEventListener("click", showHistoryModal);
|
||||
document.querySelector("[data-action='invite-agent']")?.addEventListener("click", () => showInviteModal("agent"));
|
||||
document.querySelector("[data-action='invite-bd']")?.addEventListener("click", () => showInviteModal("bd"));
|
||||
document.querySelector("#inviteSearchForm")?.addEventListener("submit", searchInviteUser);
|
||||
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.querySelectorAll("[data-close-invite]").forEach((node) => {
|
||||
node.addEventListener("click", closeInviteModal);
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
setModalOpen("#historyModal", false);
|
||||
setModalOpen("#detailsModal", false);
|
||||
closeInviteModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -338,7 +338,7 @@ button {
|
||||
}
|
||||
|
||||
.salary-card {
|
||||
min-height: 132px;
|
||||
min-height: 113px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
@ -595,12 +595,14 @@ button {
|
||||
}
|
||||
|
||||
.history-modal[hidden],
|
||||
.details-modal[hidden] {
|
||||
.details-modal[hidden],
|
||||
.invite-modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.history-modal,
|
||||
.details-modal {
|
||||
.details-modal,
|
||||
.invite-modal {
|
||||
position: fixed;
|
||||
z-index: 24;
|
||||
inset: 0;
|
||||
@ -610,13 +612,15 @@ button {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.history-backdrop {
|
||||
.history-backdrop,
|
||||
.invite-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 21, 28, 0.38);
|
||||
}
|
||||
|
||||
.history-dialog {
|
||||
.history-dialog,
|
||||
.invite-dialog {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -629,7 +633,8 @@ button {
|
||||
box-shadow: 0 18px 44px rgba(23, 31, 42, 0.2);
|
||||
}
|
||||
|
||||
.history-head {
|
||||
.history-head,
|
||||
.invite-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@ -639,7 +644,8 @@ button {
|
||||
border-bottom: 1px solid #eef0f2;
|
||||
}
|
||||
|
||||
.history-head h2 {
|
||||
.history-head h2,
|
||||
.invite-head h2 {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
@ -651,7 +657,8 @@ button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-close {
|
||||
.history-close,
|
||||
.invite-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -665,6 +672,231 @@ button {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.invite-dialog {
|
||||
max-height: 80vh;
|
||||
max-height: 80dvh;
|
||||
}
|
||||
|
||||
.invite-body {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
padding: 14px 18px max(18px, env(safe-area-inset-bottom));
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.invite-search-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.invite-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.invite-field > span {
|
||||
color: #555b64;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.invite-search-control {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.invite-search-control input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #e4e8eb;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
background: #fbfcfc;
|
||||
color: #25282e;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.invite-search-control input:focus {
|
||||
border-color: rgba(16, 188, 176, 0.72);
|
||||
box-shadow: 0 0 0 3px rgba(67, 231, 216, 0.16);
|
||||
}
|
||||
|
||||
.invite-search-control button,
|
||||
.invite-action,
|
||||
.invite-cancel {
|
||||
min-height: 34px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.invite-search-control button {
|
||||
min-width: 76px;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
background: linear-gradient(135deg, #30dfb4, #15bda9);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.invite-search-control button:disabled,
|
||||
.invite-action:disabled,
|
||||
.invite-cancel:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.invite-status {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #eafbf7;
|
||||
color: #078d80;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.invite-status.is-error {
|
||||
background: #fff0f0;
|
||||
color: #c43f3f;
|
||||
}
|
||||
|
||||
.invite-status.is-success {
|
||||
background: #ebfbf2;
|
||||
color: #128946;
|
||||
}
|
||||
|
||||
.invite-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.invite-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 86px;
|
||||
border: 1px dashed #e5e8ea;
|
||||
border-radius: 8px;
|
||||
color: #8b8d92;
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invite-user-card {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #eef0f2;
|
||||
border-radius: 8px;
|
||||
background: #fbfcfc;
|
||||
}
|
||||
|
||||
.invite-user-card.is-selected {
|
||||
border-color: rgba(16, 188, 176, 0.7);
|
||||
box-shadow: 0 0 0 3px rgba(67, 231, 216, 0.14);
|
||||
}
|
||||
|
||||
.invite-user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #d9fff5, #43e7d8);
|
||||
}
|
||||
|
||||
.invite-user-avatar img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.invite-avatar-fallback {
|
||||
color: #0b5f56;
|
||||
font-size: 18px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.invite-user-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.invite-user-name {
|
||||
overflow: hidden;
|
||||
color: #25282e;
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.invite-user-meta {
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
color: #8b8d92;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.invite-user-side {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.invite-action {
|
||||
min-width: 68px;
|
||||
padding: 0 12px;
|
||||
background: #1fcf7a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.invite-cancel {
|
||||
min-width: 68px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #ffd6d6;
|
||||
background: #fff2f2;
|
||||
color: #c74444;
|
||||
}
|
||||
|
||||
.invite-badge {
|
||||
min-width: 72px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 950;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invite-badge.pending {
|
||||
background: #fff4dc;
|
||||
color: #bd7a10;
|
||||
}
|
||||
|
||||
.invite-badge.success {
|
||||
background: #e9fbf1;
|
||||
color: #15914f;
|
||||
}
|
||||
|
||||
.history-summary {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user