chore: improve admin logs user cards

This commit is contained in:
3252a8
2026-05-30 22:15:33 +03:00
parent ea4ee4c4a7
commit c1e2fe2c95
8 changed files with 200 additions and 49 deletions
+16 -1
View File
@@ -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"}
<LogsSection {at} {fmtDate} />
<LogsSection {at} {fmtDate} onOpenUserCard={openLogsUserCard} />
{/if}
{#if active === "support"}
+116 -7
View File
@@ -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 @@
<AdminTableSkeleton
headers={logHeaders}
rows={10}
widths={["120px", "120px", "58px", "58px", "220px"]}
widths={["120px", "120px", "160px", "160px", "220px"]}
/>
{:else if !logs.length}
<AdminEmptyState tone="card"
@@ -89,12 +110,50 @@
<td class="admin-cell-mono" data-label={at("event", {}, "Событие")}
>{entry.event_type}</td
>
<td class="admin-cell-mono" data-label={at("user_short", {}, "User")}
>{entry.user_id || "—"}</td
>
<td class="admin-cell-mono" data-label={at("target_short", {}, "Target")}
>{entry.target_user_id || "—"}</td
>
<td class="admin-logs-user-cell" data-label={at("user_short", {}, "User")}>
{#if userId(entry, "user")}
<span class="admin-logs-user">
<AdminButton
class="admin-logs-user-btn"
variant="ghost"
size="icon"
title={at("payments_open_user", {}, "Open user card")}
aria-label={at("payments_open_user", {}, "Open user card")}
onclick={() => onOpenUserCard(userId(entry, "user"))}
>
<User size={14} />
</AdminButton>
<span class="admin-logs-user-meta">
<span class="admin-logs-user-name">{userDisplay(entry, "user")}</span>
<span class="admin-logs-user-id">ID {userId(entry, "user")}</span>
</span>
</span>
{:else}
<span class="admin-muted"></span>
{/if}
</td>
<td class="admin-logs-user-cell" data-label={at("target_short", {}, "Target")}>
{#if userId(entry, "target")}
<span class="admin-logs-user">
<AdminButton
class="admin-logs-user-btn"
variant="ghost"
size="icon"
title={at("payments_open_user", {}, "Open user card")}
aria-label={at("payments_open_user", {}, "Open user card")}
onclick={() => onOpenUserCard(userId(entry, "target"))}
>
<User size={14} />
</AdminButton>
<span class="admin-logs-user-meta">
<span class="admin-logs-user-name">{userDisplay(entry, "target")}</span>
<span class="admin-logs-user-id">ID {userId(entry, "target")}</span>
</span>
</span>
{:else}
<span class="admin-muted"></span>
{/if}
</td>
<td class="admin-cell-wrap" data-label={at("content", {}, "Контент")}
>{entry.content || ""}</td
>
@@ -118,3 +177,53 @@
logsStore.setPage(logsPage + 1);
}}
/>
<style>
.admin-logs-user-cell {
min-width: 150px;
}
.admin-logs-user {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.admin-logs-user-meta {
display: grid;
gap: 2px;
min-width: 0;
}
.admin-logs-user-name {
min-width: 0;
overflow: hidden;
color: var(--admin-text);
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-logs-user-id {
color: var(--admin-dim);
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.2;
white-space: nowrap;
}
.admin-logs-user-cell :global(.admin-logs-user-btn.admin-btn) {
width: 30px;
height: 30px;
min-width: 30px;
min-height: 30px;
flex-shrink: 0;
padding: 0;
border-radius: 7px;
}
.admin-logs-user-cell :global(.admin-logs-user-btn svg) {
width: 14px;
height: 14px;
}
</style>
@@ -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")}
</AdminButton>
</div>
{#if openedUserTelegramProfileLinkKind === "id"}
<small class="admin-user-telegram-profile-note"
>{openedUserTelegramProfileHint}</small
>
{/if}
</div>
</div>
@@ -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);