|
|
|
@@ -5,8 +5,11 @@
|
|
|
|
|
ArrowUp,
|
|
|
|
|
ChevronsUpDown,
|
|
|
|
|
DollarSign,
|
|
|
|
|
Sliders,
|
|
|
|
|
X,
|
|
|
|
|
UsersRound,
|
|
|
|
|
} from "$components/ui/icons.js";
|
|
|
|
|
import Dialog from "$components/ui/dialog.svelte";
|
|
|
|
|
import { Label } from "$components/ui/primitives.js";
|
|
|
|
|
import {
|
|
|
|
|
AdminBadge,
|
|
|
|
@@ -44,6 +47,7 @@
|
|
|
|
|
} = $usersStore);
|
|
|
|
|
|
|
|
|
|
const USERS_PAGE_SIZE = 25;
|
|
|
|
|
let usersFilterSheetOpen = false;
|
|
|
|
|
$: usersHasMore = users.length === USERS_PAGE_SIZE;
|
|
|
|
|
|
|
|
|
|
const USERS_FILTER_OPTIONS = [
|
|
|
|
@@ -102,6 +106,29 @@
|
|
|
|
|
{ value: "critical", label: at("premium_traffic_filter_critical", {}, "Премиум: исчерпан") },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function optionLabel(options, value) {
|
|
|
|
|
return options.find((item) => item.value === value)?.label || value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateUsersFilterState(patch) {
|
|
|
|
|
usersStore.updateState({ ...patch, usersPage: 0 });
|
|
|
|
|
usersStore.loadUsers();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetUsersFilters() {
|
|
|
|
|
updateUsersFilterState({
|
|
|
|
|
usersFilter: "all",
|
|
|
|
|
usersPanelStatus: "all",
|
|
|
|
|
usersPremiumTraffic: "all",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearUsersFilter(key) {
|
|
|
|
|
if (key === "usersFilter") updateUsersFilterState({ usersFilter: "all" });
|
|
|
|
|
if (key === "usersPanelStatus") updateUsersFilterState({ usersPanelStatus: "all" });
|
|
|
|
|
if (key === "usersPremiumTraffic") updateUsersFilterState({ usersPremiumTraffic: "all" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {Record<string, unknown> | null | undefined} pt */
|
|
|
|
|
function premiumTrafficBadgeVariant(pt) {
|
|
|
|
|
if (!pt || pt.state === "none") return "muted";
|
|
|
|
@@ -187,6 +214,24 @@
|
|
|
|
|
return fmtMoney(user?.payments_total_amount ?? 0, user?.payments_currency || "RUB");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: activeUserFilterChips = [
|
|
|
|
|
usersFilter !== "all" && {
|
|
|
|
|
key: "usersFilter",
|
|
|
|
|
label: at("filter", {}, "Фильтр"),
|
|
|
|
|
value: optionLabel(USERS_FILTER_OPTIONS, usersFilter),
|
|
|
|
|
},
|
|
|
|
|
usersPanelStatus !== "all" && {
|
|
|
|
|
key: "usersPanelStatus",
|
|
|
|
|
label: at("panel_status", {}, "Статус панели"),
|
|
|
|
|
value: optionLabel(USERS_PANEL_STATUS_OPTIONS, usersPanelStatus),
|
|
|
|
|
},
|
|
|
|
|
usersPremiumTraffic !== "all" && {
|
|
|
|
|
key: "usersPremiumTraffic",
|
|
|
|
|
label: at("premium_traffic_filter_label", {}, "Премиум трафик"),
|
|
|
|
|
value: optionLabel(USERS_PREMIUM_TRAFFIC_OPTIONS, usersPremiumTraffic),
|
|
|
|
|
},
|
|
|
|
|
].filter(Boolean);
|
|
|
|
|
$: activeUsersFilterCount = activeUserFilterChips.length;
|
|
|
|
|
$: userTableHeaders = userTableColumns().map((column) => column.label);
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
@@ -194,6 +239,65 @@
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{#snippet renderUserFilterControls()}
|
|
|
|
|
<Label.Root class="admin-toolbar-field admin-users-filter-field">
|
|
|
|
|
<span class="admin-toolbar-field-label">{at("filter", {}, "Фильтр")}</span>
|
|
|
|
|
<AdminSelect
|
|
|
|
|
value={usersFilter}
|
|
|
|
|
items={USERS_FILTER_OPTIONS}
|
|
|
|
|
class="admin-toolbar-select"
|
|
|
|
|
ariaLabel={at("filter", {}, "Фильтр")}
|
|
|
|
|
onValueChange={(value) => updateUsersFilterState({ usersFilter: value })}
|
|
|
|
|
/>
|
|
|
|
|
</Label.Root>
|
|
|
|
|
|
|
|
|
|
<Label.Root class="admin-toolbar-field admin-users-filter-field">
|
|
|
|
|
<span class="admin-toolbar-field-label">{at("panel_status", {}, "Статус панели")}</span>
|
|
|
|
|
<AdminSelect
|
|
|
|
|
value={usersPanelStatus}
|
|
|
|
|
items={USERS_PANEL_STATUS_OPTIONS}
|
|
|
|
|
class="admin-toolbar-select"
|
|
|
|
|
ariaLabel={at("panel_status", {}, "Статус панели")}
|
|
|
|
|
onValueChange={(value) => updateUsersFilterState({ usersPanelStatus: value })}
|
|
|
|
|
/>
|
|
|
|
|
</Label.Root>
|
|
|
|
|
|
|
|
|
|
<Label.Root class="admin-toolbar-field admin-users-filter-field">
|
|
|
|
|
<span class="admin-toolbar-field-label"
|
|
|
|
|
>{at("premium_traffic_filter_label", {}, "Премиум трафик")}</span
|
|
|
|
|
>
|
|
|
|
|
<AdminSelect
|
|
|
|
|
value={usersPremiumTraffic}
|
|
|
|
|
items={USERS_PREMIUM_TRAFFIC_OPTIONS}
|
|
|
|
|
class="admin-toolbar-select"
|
|
|
|
|
ariaLabel={at("premium_traffic_filter_label", {}, "Премиум трафик")}
|
|
|
|
|
onValueChange={(value) => updateUsersFilterState({ usersPremiumTraffic: value })}
|
|
|
|
|
/>
|
|
|
|
|
</Label.Root>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet renderActiveUserFilterChips()}
|
|
|
|
|
{#if activeUsersFilterCount}
|
|
|
|
|
<div class="admin-users-filter-chips" aria-label={at("active_filters", {}, "Активные фильтры")}>
|
|
|
|
|
{#each activeUserFilterChips as chip (chip.key)}
|
|
|
|
|
<span class="admin-users-filter-chip">
|
|
|
|
|
<span class="admin-users-filter-chip-text">
|
|
|
|
|
<strong>{chip.label}</strong>
|
|
|
|
|
<span>{chip.value}</span>
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
aria-label={at("clear_filter", { label: chip.label }, "Сбросить фильтр")}
|
|
|
|
|
on:click={() => clearUsersFilter(chip.key)}
|
|
|
|
|
>
|
|
|
|
|
<X size={12} />
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
<div class="admin-toolbar admin-toolbar-users">
|
|
|
|
|
<div class="admin-toolbar-search">
|
|
|
|
|
<Input
|
|
|
|
@@ -207,11 +311,28 @@
|
|
|
|
|
/>
|
|
|
|
|
<AdminButton
|
|
|
|
|
variant="primary"
|
|
|
|
|
class="admin-users-search-button"
|
|
|
|
|
onclick={() => {
|
|
|
|
|
usersStore.updateState({ usersPage: 0 });
|
|
|
|
|
usersStore.loadUsers();
|
|
|
|
|
}}>{at("find", {}, "Найти")}</AdminButton
|
|
|
|
|
>
|
|
|
|
|
<AdminButton
|
|
|
|
|
variant={activeUsersFilterCount ? "primary" : "default"}
|
|
|
|
|
class="admin-users-filter-toggle"
|
|
|
|
|
aria-label={at("users_filters_open", {}, "Открыть фильтры")}
|
|
|
|
|
aria-haspopup="dialog"
|
|
|
|
|
aria-expanded={usersFilterSheetOpen}
|
|
|
|
|
onclick={() => {
|
|
|
|
|
usersFilterSheetOpen = true;
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Sliders size={15} />
|
|
|
|
|
<span class="admin-users-filter-toggle-label">{at("filters", {}, "Фильтры")}</span>
|
|
|
|
|
{#if activeUsersFilterCount}
|
|
|
|
|
<span class="admin-users-filter-count">{activeUsersFilterCount}</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</AdminButton>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="admin-toolbar-controls">
|
|
|
|
@@ -264,8 +385,45 @@
|
|
|
|
|
<strong>{usersTotal}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{@render renderActiveUserFilterChips()}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Dialog
|
|
|
|
|
open={usersFilterSheetOpen}
|
|
|
|
|
class="admin-dialog admin-users-filter-dialog"
|
|
|
|
|
title={at("users_filters_title", {}, "Фильтры пользователей")}
|
|
|
|
|
description={at("users_filters_description", {}, "Уточните список пользователей")}
|
|
|
|
|
closeLabel={at("close_menu", {}, "Закрыть меню")}
|
|
|
|
|
onclose={() => {
|
|
|
|
|
usersFilterSheetOpen = false;
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div class="admin-users-filter-sheet-body">
|
|
|
|
|
<div class="admin-users-filter-fields admin-users-filter-fields-sheet">
|
|
|
|
|
{@render renderUserFilterControls()}
|
|
|
|
|
</div>
|
|
|
|
|
{@render renderActiveUserFilterChips()}
|
|
|
|
|
<div class="admin-users-filter-sheet-actions">
|
|
|
|
|
<AdminButton
|
|
|
|
|
variant="ghost"
|
|
|
|
|
disabled={activeUsersFilterCount === 0}
|
|
|
|
|
onclick={resetUsersFilters}
|
|
|
|
|
>
|
|
|
|
|
{at("reset", {}, "Сбросить")}
|
|
|
|
|
</AdminButton>
|
|
|
|
|
<AdminButton
|
|
|
|
|
variant="primary"
|
|
|
|
|
onclick={() => {
|
|
|
|
|
usersFilterSheetOpen = false;
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{at("done", {}, "Готово")}
|
|
|
|
|
</AdminButton>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
<div class="admin-users-table-wrap">
|
|
|
|
|
{#if usersLoading}
|
|
|
|
|
<AdminTableSkeleton
|
|
|
|
@@ -429,7 +587,111 @@
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
:global(.admin-toolbar-users .admin-toolbar-controls) {
|
|
|
|
|
grid-template-columns: repeat(3, minmax(130px, 1fr)) minmax(96px, auto);
|
|
|
|
|
grid-template-columns: repeat(3, minmax(150px, 1fr)) minmax(82px, auto);
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.admin-users-search-button) {
|
|
|
|
|
min-width: 82px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.admin-users-filter-toggle) {
|
|
|
|
|
display: none;
|
|
|
|
|
position: relative;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 7px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-count {
|
|
|
|
|
display: inline-grid;
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
place-items: center;
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: color-mix(in srgb, var(--admin-bg) 74%, transparent);
|
|
|
|
|
color: inherit;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 750;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chips {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chip {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 7px;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
min-height: 28px;
|
|
|
|
|
padding: 3px 5px 3px 10px;
|
|
|
|
|
border: 1px solid var(--admin-border);
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: color-mix(in srgb, var(--admin-muted) 8%, transparent);
|
|
|
|
|
color: var(--admin-text);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chip-text {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
max-width: 260px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chip strong {
|
|
|
|
|
color: var(--admin-muted);
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chip-text > span {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chip button {
|
|
|
|
|
display: inline-grid;
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
place-items: center;
|
|
|
|
|
border: 0;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: var(--admin-muted);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chip button:hover,
|
|
|
|
|
.admin-users-filter-chip button:focus-visible {
|
|
|
|
|
background: color-mix(in srgb, var(--admin-muted) 14%, transparent);
|
|
|
|
|
color: var(--admin-text);
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-fields-sheet,
|
|
|
|
|
.admin-users-filter-sheet-body {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-sheet-actions {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.admin-users-filter-dialog) {
|
|
|
|
|
width: min(100%, 420px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-table-wrap :global(.admin-table-wrap) {
|
|
|
|
@@ -571,6 +833,52 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
:global(.admin-toolbar-users .admin-toolbar-search) {
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.admin-toolbar-users .admin-toolbar-controls) {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.admin-users-search-button) {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
padding-inline: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.admin-users-filter-toggle) {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
min-width: 38px;
|
|
|
|
|
padding-inline: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-toggle-label {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chips {
|
|
|
|
|
gap: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-filter-chip-text {
|
|
|
|
|
max-width: min(250px, calc(100vw - 96px));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.dialog:has(.admin-users-filter-dialog)) {
|
|
|
|
|
align-items: end;
|
|
|
|
|
padding: max(12px, env(safe-area-inset-top)) 0 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.admin-users-filter-dialog) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-height: min(82dvh, 620px);
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-right: 0;
|
|
|
|
|
border-bottom: 0;
|
|
|
|
|
border-left: 0;
|
|
|
|
|
border-radius: 18px 18px 0 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-users-table-wrap :global(.admin-users-table thead) {
|
|
|
|
|
display: table-header-group;
|
|
|
|
|
}
|
|
|
|
|