diff --git a/frontend/src/admin/sections/UsersSection.svelte b/frontend/src/admin/sections/UsersSection.svelte index b9a4bc3..605651d 100644 --- a/frontend/src/admin/sections/UsersSection.svelte +++ b/frontend/src/admin/sections/UsersSection.svelte @@ -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 | 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 @@ }); +{#snippet renderUserFilterControls()} + + {at("filter", {}, "Фильтр")} + updateUsersFilterState({ usersFilter: value })} + /> + + + + {at("panel_status", {}, "Статус панели")} + updateUsersFilterState({ usersPanelStatus: value })} + /> + + + + {at("premium_traffic_filter_label", {}, "Премиум трафик")} + updateUsersFilterState({ usersPremiumTraffic: value })} + /> + +{/snippet} + +{#snippet renderActiveUserFilterChips()} + {#if activeUsersFilterCount} +
+ {#each activeUserFilterChips as chip (chip.key)} + + + {chip.label} + {chip.value} + + + + {/each} +
+ {/if} +{/snippet} +
@@ -264,8 +385,45 @@ {usersTotal}
+ + {@render renderActiveUserFilterChips()} + { + usersFilterSheetOpen = false; + }} +> +
+
+ {@render renderUserFilterControls()} +
+ {@render renderActiveUserFilterChips()} +
+ + {at("reset", {}, "Сбросить")} + + { + usersFilterSheetOpen = false; + }} + > + {at("done", {}, "Готово")} + +
+
+
+
{#if usersLoading} :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; } diff --git a/frontend/src/lib/components/patterns/admin/AdminSelect.svelte b/frontend/src/lib/components/patterns/admin/AdminSelect.svelte index b271585..53831ab 100644 --- a/frontend/src/lib/components/patterns/admin/AdminSelect.svelte +++ b/frontend/src/lib/components/patterns/admin/AdminSelect.svelte @@ -7,7 +7,10 @@ export let ariaLabel = ""; export let placeholder = ""; export let disabled = false; + export let side = "bottom"; + export let align = "start"; export let sideOffset = 6; + export let collisionPadding = 12; export let onValueChange = () => {}; let className = ""; export { className as class }; @@ -29,7 +32,7 @@ - + {#each items as item (item.value)} diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index a8ede06..2bc9294 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -4045,6 +4045,7 @@ .admin-select-content { z-index: 90; min-width: var(--bits-select-anchor-width); + max-width: calc(100vw - 24px); max-height: var(--bits-select-content-available-height, 320px); overflow: hidden; border: 1px solid var(--admin-border); @@ -4098,6 +4099,13 @@ user-select: none; } +.admin-select-item span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .admin-select-item[data-highlighted], .admin-select-item:hover { background: var(--admin-surface-2); diff --git a/locales/en.json b/locales/en.json index 3c1d8eb..4929475 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1043,6 +1043,13 @@ "admin_users_search_placeholder": "ID, @username, or email", "admin_find": "Find", "admin_filter": "Filter", + "admin_filters": "Filters", + "admin_active_filters": "Active filters", + "admin_clear_filter": "Clear {label}", + "admin_done": "Done", + "admin_users_filters_open": "Open filters", + "admin_users_filters_title": "User filters", + "admin_users_filters_description": "Refine the user list without leaving the table.", "admin_sort": "Sort", "admin_total": "Total", "admin_users_empty": "No users found", diff --git a/locales/ru.json b/locales/ru.json index a32bd98..3e21e15 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1043,6 +1043,13 @@ "admin_users_search_placeholder": "ID, @username или email", "admin_find": "Найти", "admin_filter": "Фильтр", + "admin_filters": "Фильтры", + "admin_active_filters": "Активные фильтры", + "admin_clear_filter": "Сбросить {label}", + "admin_done": "Готово", + "admin_users_filters_open": "Открыть фильтры", + "admin_users_filters_title": "Фильтры пользователей", + "admin_users_filters_description": "Уточните список пользователей, не уходя из таблицы.", "admin_sort": "Сортировка", "admin_total": "Всего", "admin_users_empty": "Никого не найдено",