diff --git a/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte b/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte index 7574afd..c5dc315 100644 --- a/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte +++ b/bot/app/web/frontend/src/admin/sections/UserDetailModal.svelte @@ -4,8 +4,12 @@ import { AdminBadge, AdminButton, + AdminEmptyState, + AdminPagination, AdminSectionHeader, AdminSelect, + AdminTable, + AdminTableSkeleton, AdminTrafficCard, } from "$components/patterns/admin/index.js"; import { @@ -51,7 +55,21 @@ userBanConfirmOpen, userMessageConfirmOpen, premiumUnlimitedDraft, + userDetailTab, + userLogs, + userLogsTotal, + userLogsPage, + userLogsLoading, + userLogsLoaded, + userLogsPageSize, } = $usersStore); + + $: userLogsHasMore = + Number(userLogsTotal || 0) > (Number(userLogsPage || 0) + 1) * Number(userLogsPageSize || 20); + + $: if (openedUser && userDetailTab === "logs" && !userLogsLoading && !userLogsLoaded) { + usersStore.loadUserLogs(0); + } {at("user_tab_activity", {}, "Активность")} + {at("user_tab_logs", {}, "Логи")} {at("user_tab_actions", {}, "Действия")} @@ -436,6 +457,94 @@ {/if} + +
+
+ {at("user_logs_section_title", {}, "Логи пользователя")} +
+
+ {at("total", {}, "Всего")} + {userLogsTotal} + usersStore.loadUserLogs(userLogsPage)} + title={at("refresh", {}, "Обновить")} + > + + {at("refresh", {}, "Обновить")} + +
+
+ +
+ {#if userLogsLoading} + + {:else if !userLogs.length} + + {at("logs_empty", {}, "Записей нет")} + + {:else} + + + + {at("date", {}, "Дата")} + {at("event", {}, "Событие")} + {at("content", {}, "Контент")} + + + + {#each userLogs as entry (entry.log_id)} + + {fmtDate(entry.timestamp)} + + + {entry.event_type || "—"} + {#if entry.is_admin_event} + {at("user_logs_admin_event", {}, "Админ")} + {/if} + {#if entry.target_user_id && entry.target_user_id !== openedUser?.user_id} + → {entry.target_user_id} + {/if} + + + + {entry.content || ""} + + + {/each} + + + {/if} +
+ + {#if userLogsLoaded && userLogsTotal > userLogsPageSize} + usersStore.setUserLogsPage(Math.max(0, userLogsPage - 1))} + onNext={() => usersStore.setUserLogsPage(userLogsPage + 1)} + /> + {/if} +
+
diff --git a/bot/app/web/frontend/src/lib/admin/stores/usersStore.js b/bot/app/web/frontend/src/lib/admin/stores/usersStore.js index 209d050..45a5a47 100644 --- a/bot/app/web/frontend/src/lib/admin/stores/usersStore.js +++ b/bot/app/web/frontend/src/lib/admin/stores/usersStore.js @@ -2,6 +2,7 @@ import { writable } from "svelte/store"; export function createUsersStore({ api, onToast, at }) { const USERS_PAGE_SIZE = 25; + const USER_LOGS_PAGE_SIZE = 20; const state = writable({ users: [], @@ -30,6 +31,14 @@ export function createUsersStore({ api, onToast, at }) { regularBonusGbDraft: "", grantTrafficGbDraft: "", grantTrafficKindDraft: "regular", + + userLogs: [], + userLogsTotal: 0, + userLogsPage: 0, + userLogsLoading: false, + userLogsLoaded: false, + userLogsUserId: null, + userLogsPageSize: USER_LOGS_PAGE_SIZE, }); let _activeRef = "stats"; // fallback if active isn't tracked @@ -96,6 +105,12 @@ export function createUsersStore({ api, onToast, at }) { userExtendDays: 30, userDetailLoading: true, userDetailTab: "subscription", + userLogs: [], + userLogsTotal: 0, + userLogsPage: 0, + userLogsLoading: false, + userLogsLoaded: false, + userLogsUserId: userId, })); if (!opts.skipPush) _pushUserPath(userId); @@ -138,11 +153,61 @@ export function createUsersStore({ api, onToast, at }) { userDeleteOpen: false, userBanConfirmOpen: false, userMessageConfirmOpen: false, + userLogs: [], + userLogsTotal: 0, + userLogsPage: 0, + userLogsLoading: false, + userLogsLoaded: false, + userLogsUserId: null, }; }); if (wasOpen && !opts.skipPush) _pushUserPath(null); } + async function loadUserLogs(page) { + let s; + state.update((st) => { + s = st; + return st; + }); + if (!s.openedUser) return; + const userId = s.openedUser.user_id; + const targetPage = Number.isFinite(page) ? Math.max(0, Math.floor(page)) : s.userLogsPage || 0; + state.update((st) => ({ + ...st, + userLogsLoading: true, + userLogsPage: targetPage, + userLogsUserId: userId, + })); + try { + const params = new URLSearchParams({ + page: String(targetPage), + page_size: String(USER_LOGS_PAGE_SIZE), + user_id: String(userId), + }); + const data = await api(`/admin/logs?${params.toString()}`); + if (data?.ok) { + state.update((st) => { + if (!st.openedUser || st.openedUser.user_id !== userId) return st; + return { + ...st, + userLogs: data.logs || [], + userLogsTotal: Number(data.total || 0), + userLogsLoaded: true, + }; + }); + } else if (data?.error) { + onToast(data.error); + } + } finally { + state.update((st) => ({ ...st, userLogsLoading: false })); + } + } + + function setUserLogsPage(page) { + loadUserLogs(page); + } + function copyToClipboard(text, successMessage = at("link_copied", {}, "Скопировано")) { if (!text) return; if (typeof navigator !== "undefined" && navigator?.clipboard?.writeText) { @@ -451,5 +516,7 @@ export function createUsersStore({ api, onToast, at }) { savePremiumTrafficOverride, saveRegularTrafficOverride, grantTraffic, + loadUserLogs, + setUserLogsPage, }; } diff --git a/locales/en.json b/locales/en.json index ffb9ee9..1dc21ed 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1075,7 +1075,11 @@ "admin_user_ref_link_copied": "Referral link copied", "admin_user_tab_subscription": "Subscription", "admin_user_tab_activity": "Activity", + "admin_user_tab_logs": "Logs", "admin_user_tab_actions": "Actions", + "admin_user_logs_section_title": "User logs", + "admin_user_logs_admin_event": "Admin", + "admin_refresh": "Refresh", "admin_user_label_active_until": "Active until", "admin_user_label_tariff": "Tariff", "admin_user_label_auto_renew": "Auto-renew", diff --git a/locales/ru.json b/locales/ru.json index 29c3b67..dc6dbdf 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1075,7 +1075,11 @@ "admin_user_ref_link_copied": "Реф. ссылка скопирована", "admin_user_tab_subscription": "Подписка", "admin_user_tab_activity": "Активность", + "admin_user_tab_logs": "Логи", "admin_user_tab_actions": "Действия", + "admin_user_logs_section_title": "Логи пользователя", + "admin_user_logs_admin_event": "Админ", + "admin_refresh": "Обновить", "admin_user_label_active_until": "Активна до", "admin_user_label_tariff": "Тариф", "admin_user_label_auto_renew": "Авто-продление",