Enhance user and media management with sorting and statistics: add sorting options to user and media listing endpoints, implement media statistics retrieval, and update frontend components for sorting and displaying media processing times. Refactor related query handlers and API types to support new features.
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Trash2, Upload } from 'lucide-react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { HttpError } from '@/shared/api/client'
|
||||
import type { ImageCategory } from '@/shared/api/types'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { deleteImage, imageUrl, listImages, uploadImage } from './api'
|
||||
|
||||
const CATEGORIES: ImageCategory[] = ['Library', 'ShowPoster', 'EpisodeStill', 'BumperBackground']
|
||||
|
||||
type ImageOrder = 'new' | 'old' | 'az' | 'za'
|
||||
|
||||
export type ImagePick = { id: string; url: string }
|
||||
|
||||
/**
|
||||
@@ -35,11 +38,30 @@ export function GalleryBrowser({
|
||||
const onError = (error: unknown) =>
|
||||
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
|
||||
|
||||
const [order, setOrder] = useState<ImageOrder>('new')
|
||||
|
||||
const { data: images, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'images', active],
|
||||
queryFn: () => listImages(active),
|
||||
})
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const arr = [...(images ?? [])]
|
||||
arr.sort((a, b) => {
|
||||
switch (order) {
|
||||
case 'old':
|
||||
return a.createdAt.localeCompare(b.createdAt)
|
||||
case 'az':
|
||||
return (a.originalFileName ?? '').localeCompare(b.originalFileName ?? '')
|
||||
case 'za':
|
||||
return (b.originalFileName ?? '').localeCompare(a.originalFileName ?? '')
|
||||
default:
|
||||
return b.createdAt.localeCompare(a.createdAt)
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}, [images, order])
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'images', active] })
|
||||
|
||||
const pick = (id: string) => {
|
||||
@@ -87,6 +109,17 @@ export function GalleryBrowser({
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{onSelect ? t('admin.gallery.pickHint') : t('admin.gallery.browseHint')}
|
||||
</span>
|
||||
<Select value={order} onValueChange={(v) => setOrder(v as ImageOrder)}>
|
||||
<SelectTrigger className="ml-auto h-8 w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="new">{t('admin.gallery.sort.newest')}</SelectItem>
|
||||
<SelectItem value="old">{t('admin.gallery.sort.oldest')}</SelectItem>
|
||||
<SelectItem value="az">{t('admin.gallery.sort.nameAsc')}</SelectItem>
|
||||
<SelectItem value="za">{t('admin.gallery.sort.nameDesc')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input
|
||||
ref={fileInput}
|
||||
type="file"
|
||||
@@ -114,7 +147,7 @@ export function GalleryBrowser({
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">{t('common.loading')}</p>
|
||||
) : images && images.length > 0 ? (
|
||||
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5">
|
||||
{images.map((img) => (
|
||||
{sorted.map((img) => (
|
||||
<div key={img.id} className="group relative">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -8,17 +8,20 @@ import { Badge, type BadgeProps } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Pager } from '@/shared/ui/pager'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { SortHeader, useTableSort } from '@/shared/ui/sortable'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { deleteMedia, listMedia } from './api'
|
||||
import { deleteMedia, getMediaStats, listMedia } from './api'
|
||||
import { UploadToShowDialog } from './UploadToShowDialog'
|
||||
import { useUploadStore } from './upload-store'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
type MediaFilter = 'active' | 'all' | 'Ready' | 'Failed'
|
||||
type MediaFilter = 'active' | 'Pending' | 'Processing' | 'all' | 'Ready' | 'Failed'
|
||||
|
||||
const filterStatuses: Record<MediaFilter, MediaAssetStatus[]> = {
|
||||
active: ['Pending', 'Processing'],
|
||||
Pending: ['Pending'],
|
||||
Processing: ['Processing'],
|
||||
all: [],
|
||||
Ready: ['Ready'],
|
||||
Failed: ['Failed'],
|
||||
@@ -48,12 +51,25 @@ export function MediaPanel() {
|
||||
const fileInputShow = useRef<HTMLInputElement>(null)
|
||||
const [filter, setFilter] = useState<MediaFilter>('active')
|
||||
const [page, setPage] = useState(1)
|
||||
const { sort, toggle } = useTableSort('created', true)
|
||||
const [filesForShow, setFilesForShow] = useState<File[] | null>(null)
|
||||
const enqueue = useUploadStore((s) => s.enqueue)
|
||||
|
||||
const sortColumn = (key: string) => {
|
||||
setPage(1)
|
||||
toggle(key)
|
||||
}
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'media', filter, page],
|
||||
queryFn: () => listMedia({ page, pageSize: PAGE_SIZE, statuses: filterStatuses[filter] }),
|
||||
queryKey: ['admin', 'media', filter, page, sort.key, sort.desc],
|
||||
queryFn: () =>
|
||||
listMedia({
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
statuses: filterStatuses[filter],
|
||||
sort: sort.key,
|
||||
desc: sort.desc,
|
||||
}),
|
||||
// Пока что-то обрабатывается — обновляем чаще, чтобы статус ехал в UI.
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.items.some((a) => a.status === 'Processing' || a.status === 'Pending')
|
||||
@@ -61,6 +77,14 @@ export function MediaPanel() {
|
||||
: false,
|
||||
})
|
||||
|
||||
const { data: stats } = useQuery({
|
||||
queryKey: ['admin', 'media', 'stats'],
|
||||
queryFn: getMediaStats,
|
||||
// Пока есть незавершённая работа — освежаем чипы очереди/обработки.
|
||||
refetchInterval: (query) =>
|
||||
(query.state.data?.queued ?? 0) + (query.state.data?.processing ?? 0) > 0 ? 4000 : 15000,
|
||||
})
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'media'] })
|
||||
const onError = (error: unknown) =>
|
||||
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
|
||||
@@ -84,11 +108,32 @@ export function MediaPanel() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">{t('admin.media.filterActive')}</SelectItem>
|
||||
<SelectItem value="Pending">{t('admin.media.statuses.Pending')}</SelectItem>
|
||||
<SelectItem value="Processing">{t('admin.media.statuses.Processing')}</SelectItem>
|
||||
<SelectItem value="all">{t('admin.media.filterAll')}</SelectItem>
|
||||
<SelectItem value="Ready">{t('admin.media.statuses.Ready')}</SelectItem>
|
||||
<SelectItem value="Failed">{t('admin.media.statuses.Failed')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{stats && (
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span title={t('admin.media.stats.queued')}>
|
||||
{t('admin.media.stats.queuedShort')}:{' '}
|
||||
<span className="text-foreground tabular-nums">{stats.queued}</span>
|
||||
</span>
|
||||
<span title={t('admin.media.stats.processing')}>
|
||||
{t('admin.media.stats.processingShort')}:{' '}
|
||||
<span className="text-foreground tabular-nums">{stats.processing}</span>
|
||||
</span>
|
||||
<span title={t('admin.media.stats.average')}>
|
||||
{t('admin.media.stats.averageShort')}:{' '}
|
||||
<span className="text-foreground tabular-nums">
|
||||
{formatDuration(stats.averageProcessingSeconds)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
@@ -134,17 +179,43 @@ export function MediaPanel() {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b border-border text-left text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.media.name')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.media.status')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.media.duration')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.media.resolution')}</th>
|
||||
<SortHeader
|
||||
label={t('admin.media.name')}
|
||||
sortKey="name"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.media.status')}
|
||||
sortKey="status"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.media.duration')}
|
||||
sortKey="duration"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.media.resolution')}
|
||||
sortKey="resolution"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.media.processingTime')}
|
||||
sortKey="processing"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<th className="px-4 py-2 font-medium">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading && (
|
||||
<tr>
|
||||
<td className="px-4 py-3 text-muted-foreground" colSpan={5}>
|
||||
<td className="px-4 py-3 text-muted-foreground" colSpan={6}>
|
||||
{t('common.loading')}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -158,7 +229,7 @@ export function MediaPanel() {
|
||||
))}
|
||||
{data && data.items.length === 0 && !isLoading && (
|
||||
<tr>
|
||||
<td className="px-4 py-3 text-muted-foreground" colSpan={5}>
|
||||
<td className="px-4 py-3 text-muted-foreground" colSpan={6}>
|
||||
{t('admin.media.empty')}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -190,6 +261,9 @@ function MediaRow({ asset, onDelete }: { asset: MediaAssetDto; onDelete: () => v
|
||||
<td className="px-4 py-2 text-muted-foreground">
|
||||
{asset.width && asset.height ? `${asset.width}×${asset.height}` : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-muted-foreground tabular-nums">
|
||||
{asset.status === 'Ready' ? formatDuration(asset.processingSeconds) : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Button size="sm" variant="destructive" onClick={onDelete}>
|
||||
{t('common.delete')}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
CreatedIdResponse,
|
||||
MediaAssetDto,
|
||||
MediaAssetStatus,
|
||||
MediaStatsDto,
|
||||
PagedList,
|
||||
} from '@/shared/api/types'
|
||||
|
||||
@@ -11,6 +12,8 @@ export type ListMediaParams = {
|
||||
pageSize: number
|
||||
statuses?: MediaAssetStatus[]
|
||||
search?: string
|
||||
sort?: string
|
||||
desc?: boolean
|
||||
}
|
||||
|
||||
export function listMedia(params: ListMediaParams) {
|
||||
@@ -20,9 +23,15 @@ export function listMedia(params: ListMediaParams) {
|
||||
})
|
||||
for (const status of params.statuses ?? []) query.append('status', status)
|
||||
if (params.search) query.set('search', params.search)
|
||||
if (params.sort) query.set('sort', params.sort)
|
||||
if (params.desc) query.set('desc', 'true')
|
||||
return apiRequest<PagedList<MediaAssetDto>>(`/admin/media?${query.toString()}`)
|
||||
}
|
||||
|
||||
export function getMediaStats() {
|
||||
return apiRequest<MediaStatsDto>('/admin/media/stats')
|
||||
}
|
||||
|
||||
/**
|
||||
* Дозагружает ВСЕ страницы медиа (для пикеров с клиентской фильтрацией/сортировкой — кандидаты серий,
|
||||
* пул рекламы), чтобы ничего не терялось молча за фиксированным pageSize. Есть предохранитель `cap`:
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@/shared/ui/dialog'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { SortHeader, sortRows, useTableSort } from '@/shared/ui/sortable'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { createRole, deleteRole, listRoles, updateRole } from './api'
|
||||
|
||||
@@ -27,6 +28,11 @@ export function RolesPanel() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const { data: roles, isLoading } = useQuery({ queryKey: ['admin', 'roles'], queryFn: listRoles })
|
||||
const { sort, toggle } = useTableSort('name', false)
|
||||
const sortedRoles = sortRows(roles ?? [], sort, {
|
||||
name: (r) => r.name.toLowerCase(),
|
||||
system: (r) => r.isSystem,
|
||||
})
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'roles'] })
|
||||
|
||||
@@ -97,8 +103,18 @@ export function RolesPanel() {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b border-border text-left text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.roles.name')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.roles.system')}</th>
|
||||
<SortHeader
|
||||
label={t('admin.roles.name')}
|
||||
sortKey="name"
|
||||
sort={sort}
|
||||
onToggle={toggle}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.roles.system')}
|
||||
sortKey="system"
|
||||
sort={sort}
|
||||
onToggle={toggle}
|
||||
/>
|
||||
<th className="px-4 py-2 font-medium">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -110,7 +126,7 @@ export function RolesPanel() {
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{roles?.map((role) => (
|
||||
{sortedRoles.map((role) => (
|
||||
<tr key={role.id} className="border-b border-border last:border-0">
|
||||
<td className="px-4 py-2">{role.name}</td>
|
||||
<td className="px-4 py-2">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button } from '@/shared/ui/button'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Pager } from '@/shared/ui/pager'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { SortHeader, sortRows, useTableSort } from '@/shared/ui/sortable'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { createShow, deleteShow, listShows } from './api'
|
||||
|
||||
@@ -22,19 +23,31 @@ export function ShowsPanel() {
|
||||
const [kind, setKind] = useState<ShowKind>('Series')
|
||||
const [query, setQuery] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const { sort, toggle } = useTableSort('name', false)
|
||||
const sortColumn = (key: string) => {
|
||||
setPage(1)
|
||||
toggle(key)
|
||||
}
|
||||
|
||||
const { data, isLoading } = useQuery({ queryKey: ['admin', 'shows'], queryFn: listShows })
|
||||
|
||||
// Список шоу обычно умещается в одну загрузку — фильтруем и листаем на клиенте (пикеры берут всё).
|
||||
// Список шоу обычно умещается в одну загрузку — фильтруем, сортируем и листаем на клиенте.
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase()
|
||||
const all = data ?? []
|
||||
if (!q) return all
|
||||
return all.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(q) || (s.originalName ?? '').toLowerCase().includes(q),
|
||||
)
|
||||
}, [data, query])
|
||||
const matched = q
|
||||
? all.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(q) || (s.originalName ?? '').toLowerCase().includes(q),
|
||||
)
|
||||
: all
|
||||
return sortRows(matched, sort, {
|
||||
name: (s) => s.name.toLowerCase(),
|
||||
kind: (s) => s.kind,
|
||||
seasons: (s) => s.seasonCount,
|
||||
episodes: (s) => s.episodeCount,
|
||||
})
|
||||
}, [data, query, sort])
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
|
||||
const pageItems = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'shows'] })
|
||||
@@ -102,10 +115,30 @@ export function ShowsPanel() {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b border-border text-left text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.shows.name')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.shows.kind')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.shows.seasons')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.shows.episodes')}</th>
|
||||
<SortHeader
|
||||
label={t('admin.shows.name')}
|
||||
sortKey="name"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.shows.kind')}
|
||||
sortKey="kind"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.shows.seasons')}
|
||||
sortKey="seasons"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.shows.episodes')}
|
||||
sortKey="episodes"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<th className="px-4 py-2 font-medium">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { SortHeader, useTableSort } from '@/shared/ui/sortable'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import type { UserSummaryDto } from '@/shared/api/types'
|
||||
import { changeUserRole } from '@/features/admin/roles/api'
|
||||
@@ -29,6 +30,11 @@ export function UsersPanel() {
|
||||
const [page, setPage] = useState(1)
|
||||
const [search, setSearch] = useState('')
|
||||
const [roleId, setRoleId] = useState<string>('')
|
||||
const { sort, toggle } = useTableSort('created', true)
|
||||
const sortColumn = (key: string) => {
|
||||
setPage(1)
|
||||
toggle(key)
|
||||
}
|
||||
const [newUserName, setNewUserName] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [newRoleId, setNewRoleId] = useState('')
|
||||
@@ -36,8 +42,16 @@ export function UsersPanel() {
|
||||
|
||||
const { data: roles } = useQuery({ queryKey: ['admin', 'roles'], queryFn: listRoles })
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'users', page, search, roleId],
|
||||
queryFn: () => listUsers({ page, pageSize: PAGE_SIZE, search: search || undefined, roleId: roleId || undefined }),
|
||||
queryKey: ['admin', 'users', page, search, roleId, sort.key, sort.desc],
|
||||
queryFn: () =>
|
||||
listUsers({
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
search: search || undefined,
|
||||
roleId: roleId || undefined,
|
||||
sort: sort.key,
|
||||
desc: sort.desc,
|
||||
}),
|
||||
})
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
|
||||
@@ -167,10 +181,30 @@ export function UsersPanel() {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b border-border text-left text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.users.userName')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.users.role')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.users.status')}</th>
|
||||
<th className="px-4 py-2 font-medium">{t('admin.users.createdAt')}</th>
|
||||
<SortHeader
|
||||
label={t('admin.users.userName')}
|
||||
sortKey="username"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.users.role')}
|
||||
sortKey="role"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.users.status')}
|
||||
sortKey="blocked"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<SortHeader
|
||||
label={t('admin.users.createdAt')}
|
||||
sortKey="created"
|
||||
sort={sort}
|
||||
onToggle={sortColumn}
|
||||
/>
|
||||
<th className="px-4 py-2 font-medium">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -7,6 +7,8 @@ export type ListUsersParams = {
|
||||
search?: string
|
||||
roleId?: string
|
||||
isBlocked?: boolean
|
||||
sort?: string
|
||||
desc?: boolean
|
||||
}
|
||||
|
||||
export function listUsers(params: ListUsersParams) {
|
||||
@@ -17,6 +19,8 @@ export function listUsers(params: ListUsersParams) {
|
||||
if (params.search) query.set('search', params.search)
|
||||
if (params.roleId) query.set('roleId', params.roleId)
|
||||
if (params.isBlocked !== undefined) query.set('isBlocked', String(params.isBlocked))
|
||||
if (params.sort) query.set('sort', params.sort)
|
||||
if (params.desc) query.set('desc', 'true')
|
||||
|
||||
return apiRequest<PagedList<UserSummaryDto>>(`/admin/users?${query.toString()}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user