fix: multi-tariff filter now works server-side + trial-only checkbox

- Send comma-separated tariff_ids to backend (was only single ID)
  — server now filters with IN() operator, pagination correct
- Remove client-side tariff filtering (server handles it)
- Add "Только триал" checkbox filter: amber checkbox, filters users
  with trial subscriptions client-side
- Remove "trial" from status dropdown (was not working via
  subscription_status server filter)
- i18n: trialOnly key (ru + en)
This commit is contained in:
Fringg
2026-04-24 07:16:53 +03:00
parent efca40d9c8
commit 98871af656
3 changed files with 41 additions and 9 deletions

View File

@@ -2086,6 +2086,7 @@
"allStatuses": "All statuses", "allStatuses": "All statuses",
"allTariffs": "All tariffs", "allTariffs": "All tariffs",
"allGroups": "All groups", "allGroups": "All groups",
"trialOnly": "Trial only",
"tariffsSelected": "{{count}} tariffs", "tariffsSelected": "{{count}} tariffs",
"selectAll": "Select all", "selectAll": "Select all",
"deselectAll": "Deselect all" "deselectAll": "Deselect all"

View File

@@ -3834,6 +3834,7 @@
"allStatuses": "Все статусы", "allStatuses": "Все статусы",
"allTariffs": "Все тарифы", "allTariffs": "Все тарифы",
"allGroups": "Все группы", "allGroups": "Все группы",
"trialOnly": "Только триал",
"tariffsSelected": "{{count}} тарифов", "tariffsSelected": "{{count}} тарифов",
"selectAll": "Выбрать все", "selectAll": "Выбрать все",
"deselectAll": "Снять все" "deselectAll": "Снять все"

View File

@@ -1403,6 +1403,7 @@ export default function AdminBulkActions() {
const [searchInput, setSearchInput] = useState(''); const [searchInput, setSearchInput] = useState('');
const [committedSearch, setCommittedSearch] = useState(''); const [committedSearch, setCommittedSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<SubscriptionStatusFilter>(''); const [statusFilter, setStatusFilter] = useState<SubscriptionStatusFilter>('');
const [trialOnly, setTrialOnly] = useState(false);
const [tariffFilter, setTariffFilter] = useState<number[]>([]); const [tariffFilter, setTariffFilter] = useState<number[]>([]);
const [promoGroupFilter, setPromoGroupFilter] = useState(''); const [promoGroupFilter, setPromoGroupFilter] = useState('');
@@ -1444,7 +1445,7 @@ export default function AdminBulkActions() {
}; };
if (committedSearch) params.search = committedSearch; if (committedSearch) params.search = committedSearch;
if (statusFilter) params.subscription_status = statusFilter; if (statusFilter) params.subscription_status = statusFilter;
if (tariffFilter.length === 1) params.tariff_id = tariffFilter[0]; if (tariffFilter.length > 0) params.tariff_id = tariffFilter.join(',');
if (promoGroupFilter) params.promo_group_id = Number(promoGroupFilter); if (promoGroupFilter) params.promo_group_id = Number(promoGroupFilter);
const data = await adminUsersApi.getUsers( const data = await adminUsersApi.getUsers(
@@ -1980,12 +1981,17 @@ export default function AdminBulkActions() {
// When multiple tariffs are selected, filter users client-side // When multiple tariffs are selected, filter users client-side
// (server only supports single tariff_id filter) // (server only supports single tariff_id filter)
const filteredUsers = useMemo(() => { const filteredUsers = useMemo(() => {
if (tariffFilter.length <= 1) return users; let result = users;
return users.filter((u) => {
const subs = u.subscriptions ?? []; // Trial-only filter: show users with trial subscription
return subs.some((s) => s.tariff_id !== null && tariffFilter.includes(s.tariff_id)); if (trialOnly) {
}); result = result.filter(
}, [users, tariffFilter]); (u) => u.subscription_is_trial || (u.subscriptions ?? []).some((s) => s.status === 'trial'),
);
}
return result;
}, [users, trialOnly]);
const table = useReactTable({ const table = useReactTable({
data: filteredUsers, data: filteredUsers,
@@ -2005,7 +2011,6 @@ export default function AdminBulkActions() {
{ value: '', label: t('admin.bulkActions.filters.allStatuses') }, { value: '', label: t('admin.bulkActions.filters.allStatuses') },
{ value: 'active', label: t('admin.bulkActions.statuses.active') }, { value: 'active', label: t('admin.bulkActions.statuses.active') },
{ value: 'expired', label: t('admin.bulkActions.statuses.expired') }, { value: 'expired', label: t('admin.bulkActions.statuses.expired') },
{ value: 'trial', label: t('admin.bulkActions.statuses.trial') },
{ value: 'limited', label: t('admin.bulkActions.statuses.limited') }, { value: 'limited', label: t('admin.bulkActions.statuses.limited') },
{ value: 'disabled', label: t('admin.bulkActions.statuses.disabled') }, { value: 'disabled', label: t('admin.bulkActions.statuses.disabled') },
]; ];
@@ -2074,7 +2079,7 @@ export default function AdminBulkActions() {
</div> </div>
</form> </form>
{/* Filter dropdowns */} {/* Filter dropdowns + trial toggle */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<DropdownSelect <DropdownSelect
value={statusFilter} value={statusFilter}
@@ -2093,6 +2098,31 @@ export default function AdminBulkActions() {
onChange={handlePromoGroupFilterChange} onChange={handlePromoGroupFilterChange}
/> />
</div> </div>
{/* Trial filter checkbox */}
<label className="inline-flex cursor-pointer items-center gap-2">
<button
onClick={() => {
setTrialOnly((prev) => !prev);
setOffset(0);
clearAllSelections();
}}
className={cn(
'flex h-5 w-5 items-center justify-center rounded-md border-2 transition-all duration-150',
trialOnly
? 'border-amber-500 bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.4)]'
: 'border-dark-500 bg-dark-700/60 hover:border-amber-500/50 hover:bg-dark-600/60',
)}
aria-pressed={trialOnly}
>
{trialOnly && <CheckIcon />}
</button>
<span
className={cn('text-sm', trialOnly ? 'font-medium text-amber-400' : 'text-dark-400')}
>
{t('admin.bulkActions.filters.trialOnly')}
</span>
</label>
</div> </div>
{/* Table */} {/* Table */}