diff --git a/h5/hyapp/bd-leader-center/index.html b/h5/hyapp/bd-leader-center/index.html index 6c07f73..8d841b7 100644 --- a/h5/hyapp/bd-leader-center/index.html +++ b/h5/hyapp/bd-leader-center/index.html @@ -4,7 +4,7 @@ BD Leader Center - +
@@ -116,21 +116,21 @@ - - @@ -183,6 +183,30 @@
+ +
- + diff --git a/h5/hyapp/bd-leader-center/script.js b/h5/hyapp/bd-leader-center/script.js index e043128..8529927 100644 --- a/h5/hyapp/bd-leader-center/script.js +++ b/h5/hyapp/bd-leader-center/script.js @@ -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(); } }); } diff --git a/h5/hyapp/bd-leader-center/style.css b/h5/hyapp/bd-leader-center/style.css index fd2aebb..94da82d 100644 --- a/h5/hyapp/bd-leader-center/style.css +++ b/h5/hyapp/bd-leader-center/style.css @@ -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;