diff --git a/backend/bot/app/web/admin_api_impl/common.py b/backend/bot/app/web/admin_api_impl/common.py index 624515d..b43ac82 100644 --- a/backend/bot/app/web/admin_api_impl/common.py +++ b/backend/bot/app/web/admin_api_impl/common.py @@ -143,12 +143,18 @@ def _payment_traffic_gb_split(payment: Payment) -> Tuple[Optional[float], Option return None, None -def _payment_user_display_label(loaded_user: Any, payment_user_id: int) -> str: - """Human-facing name for payments tables: TG profile name, else email, else user id.""" - if loaded_user is None: - return str(payment_user_id) +def _user_display_label( + loaded_user: Any, + fallback_user_id: Optional[int], + *, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + username: Optional[str] = None, + email: Optional[str] = None, +) -> Optional[str]: + """Human-facing name: TG profile name, else email, else user id.""" tid = getattr(loaded_user, "telegram_id", None) - if tid is not None: + if loaded_user is not None and tid is not None: fn = (getattr(loaded_user, "first_name", None) or "").strip() ln = (getattr(loaded_user, "last_name", None) or "").strip() full = f"{fn} {ln}".strip() @@ -157,10 +163,30 @@ def _payment_user_display_label(loaded_user: Any, payment_user_id: int) -> str: un = (getattr(loaded_user, "username", None) or "").strip() if un: return un if un.startswith("@") else f"@{un}" - return str(payment_user_id) - email = (getattr(loaded_user, "email", None) or "").strip() - if email: - return email + elif loaded_user is not None: + email = (getattr(loaded_user, "email", None) or "").strip() + if email: + return email + fn = (first_name or "").strip() + ln = (last_name or "").strip() + full = f"{fn} {ln}".strip() + if full: + return full + un = (username or "").strip() + if un: + return un if un.startswith("@") else f"@{un}" + email_value = (email or "").strip() + if email_value: + return email_value + if fallback_user_id is None: + return None + return str(fallback_user_id) + + +def _payment_user_display_label(loaded_user: Any, payment_user_id: int) -> str: + label = _user_display_label(loaded_user, payment_user_id) + if label: + return label return str(payment_user_id) @@ -229,16 +255,27 @@ def _serialize_ad(campaign: AdCampaign, totals: Optional[Dict[str, Any]] = None) def _serialize_log(entry: MessageLog) -> Dict[str, Any]: + author_user = entry.__dict__.get("author_user") + target_user = entry.__dict__.get("target_user") + user_id = int(entry.user_id) if entry.user_id is not None else None + target_user_id = int(entry.target_user_id) if entry.target_user_id is not None else None return { "log_id": int(entry.log_id), - "user_id": int(entry.user_id) if entry.user_id else None, + "user_id": user_id, + "user_label": _user_display_label( + author_user, + user_id, + first_name=entry.telegram_first_name, + username=entry.telegram_username, + ), "telegram_username": entry.telegram_username, "telegram_first_name": entry.telegram_first_name, - "email": getattr(getattr(entry, "author_user", None), "email", None), + "email": getattr(author_user, "email", None), "event_type": entry.event_type, "content": entry.content, "is_admin_event": bool(entry.is_admin_event), - "target_user_id": int(entry.target_user_id) if entry.target_user_id else None, + "target_user_id": target_user_id, + "target_user_label": _user_display_label(target_user, target_user_id), "timestamp": entry.timestamp.isoformat() if entry.timestamp else None, } diff --git a/frontend/src/admin/AdminPanel.svelte b/frontend/src/admin/AdminPanel.svelte index f7d7d53..ab381fe 100644 --- a/frontend/src/admin/AdminPanel.svelte +++ b/frontend/src/admin/AdminPanel.svelte @@ -389,6 +389,21 @@ usersStore.openUser(uid, { pathContext: "payments" }); } + function openLogsUserCard(userId) { + const uid = Number(userId); + if (!Number.isFinite(uid) || uid === 0) return; + const next = normalizeSection("logs"); + sidebarOpen = false; + if (active !== next) { + active = next; + paymentsStore.closePayment({ skipPush: true }); + supportStore.closeTicketView({ skipPush: true }); + onSectionChange(next); + } + usersStore.setActive(next); + usersStore.openUser(uid, { skipPush: true, pathContext: "logs" }); + } + function openUserCard(userId) { const uid = Number(userId); if (!Number.isFinite(uid) || uid === 0) return; @@ -801,7 +816,7 @@ {/if} {#if active === "logs"} - + {/if} {#if active === "support"} diff --git a/frontend/src/admin/sections/LogsSection.svelte b/frontend/src/admin/sections/LogsSection.svelte index 3b32d00..d0de85b 100644 --- a/frontend/src/admin/sections/LogsSection.svelte +++ b/frontend/src/admin/sections/LogsSection.svelte @@ -8,9 +8,11 @@ AdminTable, AdminTableSkeleton, } from "$components/patterns/admin/index.js"; + import { User } from "$components/ui/icons.js"; export let at; export let fmtDate; + export let onOpenUserCard = () => {}; const logsStore = getContext("logsStore"); @@ -25,6 +27,25 @@ at("content", {}, "Контент"), ]; + function userDisplay(entry, kind) { + const id = kind === "target" ? entry.target_user_id : entry.user_id; + const label = kind === "target" ? entry.target_user_label : entry.user_label; + if (label) return label; + if (kind !== "target") { + if (entry.telegram_first_name) return entry.telegram_first_name; + if (entry.telegram_username) { + const username = String(entry.telegram_username); + return username.startsWith("@") ? username : `@${username}`; + } + if (entry.email) return entry.email; + } + return id || "—"; + } + + function userId(entry, kind) { + return kind === "target" ? entry.target_user_id : entry.user_id; + } + onMount(() => { logsStore.loadLogs(); }); @@ -65,7 +86,7 @@ {:else if !logs.length} {entry.event_type} - {entry.user_id || "—"} - {entry.target_user_id || "—"} + + {#if userId(entry, "user")} + + onOpenUserCard(userId(entry, "user"))} + > + + + + {userDisplay(entry, "user")} + ID {userId(entry, "user")} + + + {:else} + + {/if} + + + {#if userId(entry, "target")} + + onOpenUserCard(userId(entry, "target"))} + > + + + + {userDisplay(entry, "target")} + ID {userId(entry, "target")} + + + {:else} + + {/if} + {entry.content || ""} @@ -118,3 +177,53 @@ logsStore.setPage(logsPage + 1); }} /> + + diff --git a/frontend/src/admin/sections/UserDetailModal.svelte b/frontend/src/admin/sections/UserDetailModal.svelte index 86a293c..a424b25 100644 --- a/frontend/src/admin/sections/UserDetailModal.svelte +++ b/frontend/src/admin/sections/UserDetailModal.svelte @@ -81,11 +81,7 @@ $: openedUserTelegramProfileLinkKind = openedUser ? userTelegramProfileLinkKind(openedUser) : ""; $: openedUserTelegramProfileHint = openedUserTelegramProfileLinkKind === "id" - ? at( - "user_open_tg_profile_id_hint", - {}, - "Профиль будет открыт по Telegram ID. Telegram может заблокировать переход из-за настроек приватности пользователя или ограничений клиента." - ) + ? at("user_open_tg_profile_id_hint", {}, "Бот отправит кнопку профиля в Telegram") : at("user_open_tg_profile_hint", {}, "Открыть профиль Telegram"); $: if (openedUser && userDetailTab === "logs" && !userLogsLoading && !userLogsLoaded) { @@ -189,11 +185,6 @@ {at("user_open_tg_profile", {}, "Открыть Telegram")} - {#if openedUserTelegramProfileLinkKind === "id"} - {openedUserTelegramProfileHint} - {/if} @@ -1168,12 +1159,6 @@ gap: 8px; margin-top: 6px; } - .admin-user-telegram-profile-note { - display: block; - margin-top: 2px; - color: var(--admin-dim); - line-height: 1.35; - } :global(.admin-avatar-dialog) { display: grid; grid-template-rows: auto minmax(0, 1fr); diff --git a/frontend/src/lib/webapp/demoDataset.js b/frontend/src/lib/webapp/demoDataset.js index 2878686..d70b3d6 100644 --- a/frontend/src/lib/webapp/demoDataset.js +++ b/frontend/src/lib/webapp/demoDataset.js @@ -101636,22 +101636,18 @@ export const DEMO_DATASET = { audience: "internal", values: { ru: { - base: "Пользователь без username: бот отправит вам в Telegram сообщение с кнопкой открытия профиля.", - fallback: - "Пользователь без username: бот отправит вам в Telegram сообщение с кнопкой открытия профиля.", - effective: - "Пользователь без username: бот отправит вам в Telegram сообщение с кнопкой открытия профиля.", + base: "Бот отправит кнопку профиля в Telegram.", + fallback: "Бот отправит кнопку профиля в Telegram.", + effective: "Бот отправит кнопку профиля в Telegram.", override: "", overridden: false, updated_at: null, updated_by: null, }, en: { - base: "This user has no username: the bot will send you a Telegram message with a profile button.", - fallback: - "Пользователь без username: бот отправит вам в Telegram сообщение с кнопкой открытия профиля.", - effective: - "This user has no username: the bot will send you a Telegram message with a profile button.", + base: "The bot will send a profile button in Telegram.", + fallback: "Бот отправит кнопку профиля в Telegram.", + effective: "The bot will send a profile button in Telegram.", override: "", overridden: false, updated_at: null, diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index 0cdb209..3dd00c4 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -2760,6 +2760,15 @@ max-height: min(100%, 760px); } +.admin-user-dialog .dialog-body-scroll { + margin-right: -10px; + padding-right: 10px; +} + +.admin-user-dialog .dialog-body-scroll > .scroll-area__viewport { + padding-right: 10px; +} + .admin-user-dialog-body { display: grid; grid-template-columns: minmax(0, 1fr); @@ -4131,7 +4140,7 @@ .admin-user-summary { display: grid; grid-template-columns: 56px minmax(0, 1fr); - align-items: center; + align-items: start; gap: 14px; padding: 14px 14px 16px; border-radius: 12px; diff --git a/locales/en.json b/locales/en.json index a931f8c..0f6f364 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1200,7 +1200,7 @@ "admin_user_avatar_title": "Avatar", "admin_user_open_tg_profile": "Open Telegram", "admin_user_open_tg_profile_hint": "Open Telegram profile", - "admin_user_open_tg_profile_id_hint": "This user has no username: the bot will send you a Telegram message with a profile button.", + "admin_user_open_tg_profile_id_hint": "The bot will send a profile button in Telegram.", "admin_user_tg_profile_unavailable": "Telegram profile link is unavailable", "admin_user_tg_profile_link_sent": "Link sent to Telegram", "admin_user_tg_profile_link_failed": "Failed to send link", diff --git a/locales/ru.json b/locales/ru.json index 0073410..bb7164f 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1200,7 +1200,7 @@ "admin_user_avatar_title": "Аватар", "admin_user_open_tg_profile": "Открыть Telegram", "admin_user_open_tg_profile_hint": "Открыть профиль Telegram", - "admin_user_open_tg_profile_id_hint": "Пользователь без username: бот отправит вам в Telegram сообщение с кнопкой открытия профиля.", + "admin_user_open_tg_profile_id_hint": "Бот отправит кнопку профиля в Telegram.", "admin_user_tg_profile_unavailable": "Ссылка на профиль Telegram недоступна", "admin_user_tg_profile_link_sent": "Ссылка отправлена в Telegram", "admin_user_tg_profile_link_failed": "Не удалось отправить ссылку",