diff --git a/frontend/src/pages/admin/AdminGrantItemPage.tsx b/frontend/src/pages/admin/AdminGrantItemPage.tsx
new file mode 100644
index 0000000..1f73427
--- /dev/null
+++ b/frontend/src/pages/admin/AdminGrantItemPage.tsx
@@ -0,0 +1,404 @@
+import { useState, useEffect } from 'react'
+import { useParams, useNavigate } from 'react-router-dom'
+import { shopApi, adminApi } from '@/api'
+import { useToast } from '@/store/toast'
+import { GlassCard, NeonButton, FramePreview } from '@/components/ui'
+import {
+ Loader2, Gift, ArrowLeft, Package,
+ Frame, Type, Palette, Image, Zap, SkipForward,
+ Minus, Plus, Shuffle, Dice5, Copy, Undo2, X, XCircle
+} from 'lucide-react'
+import type { ShopItem, ShopItemType, ShopItemPublic, AdminUser } from '@/types'
+import { RARITY_COLORS, RARITY_NAMES } from '@/types'
+import clsx from 'clsx'
+
+const ITEM_TYPE_ICONS: Record
= {
+ frame: ,
+ title: ,
+ name_color: ,
+ background: ,
+ consumable: ,
+}
+
+const CONSUMABLE_ICONS: Record = {
+ skip: ,
+ skip_exile: ,
+ boost: ,
+ wild_card: ,
+ lucky_dice: ,
+ copycat: ,
+ undo: ,
+}
+
+const ITEM_TYPE_LABELS: Record = {
+ all: 'Все',
+ consumable: 'Расходники',
+ frame: 'Рамки',
+ title: 'Титулы',
+ name_color: 'Цвета',
+ background: 'Фоны',
+}
+
+interface GrantItemCardProps {
+ item: ShopItem
+ onGrant: (item: ShopItem) => void
+}
+
+function GrantItemCard({ item, onGrant }: GrantItemCardProps) {
+ const rarityColors = RARITY_COLORS[item.rarity]
+
+ const getItemPreview = () => {
+ if (item.item_type === 'consumable') {
+ return CONSUMABLE_ICONS[item.code] ||
+ }
+
+ if (item.item_type === 'name_color') {
+ const data = item.asset_data as { style?: string; color?: string; gradient?: string[] } | null
+
+ if (data?.style === 'gradient' && data.gradient) {
+ return (
+
+ )
+ }
+
+ if (data?.style === 'animated') {
+ return (
+
+ )
+ }
+
+ const solidColor = data?.color || '#ffffff'
+ return (
+
+ )
+ }
+
+ if (item.item_type === 'background') {
+ const data = item.asset_data as { type?: string; color?: string; gradient?: string[] } | null
+ let bgStyle: React.CSSProperties = {}
+
+ if (data?.type === 'solid' && data.color) {
+ bgStyle = { backgroundColor: data.color }
+ } else if (data?.type === 'gradient' && data.gradient) {
+ bgStyle = { background: `linear-gradient(135deg, ${data.gradient.join(', ')})` }
+ }
+
+ return (
+
+ )
+ }
+
+ if (item.item_type === 'frame') {
+ const frameItem: ShopItemPublic = {
+ id: item.id,
+ code: item.code,
+ name: item.name,
+ item_type: item.item_type,
+ rarity: item.rarity,
+ asset_data: item.asset_data,
+ }
+ return
+ }
+
+ if (item.item_type === 'title' && item.asset_data?.text) {
+ return (
+
+ {item.asset_data.text as string}
+
+ )
+ }
+
+ return ITEM_TYPE_ICONS[item.item_type]
+ }
+
+ return (
+
+ {/* Rarity badge */}
+
+ {RARITY_NAMES[item.rarity]}
+
+
+ {/* Item preview */}
+
+ {getItemPreview()}
+
+
+ {/* Item info */}
+ {item.name}
+
+ {item.description}
+
+
+ {/* Grant button */}
+ onGrant(item)}
+ className="w-full"
+ icon={}
+ >
+ Выдать
+
+
+ )
+}
+
+export function AdminGrantItemPage() {
+ const { userId } = useParams<{ userId: string }>()
+ const navigate = useNavigate()
+ const toast = useToast()
+
+ const [user, setUser] = useState(null)
+ const [items, setItems] = useState([])
+ const [isLoading, setIsLoading] = useState(true)
+ const [activeTab, setActiveTab] = useState('all')
+
+ // Grant modal
+ const [grantItem, setGrantItem] = useState(null)
+ const [grantQuantity, setGrantQuantity] = useState(1)
+ const [grantReason, setGrantReason] = useState('')
+ const [isGranting, setIsGranting] = useState(false)
+
+ useEffect(() => {
+ loadData()
+ }, [userId])
+
+ const loadData = async () => {
+ if (!userId) return
+ setIsLoading(true)
+ try {
+ const [userData, itemsData] = await Promise.all([
+ adminApi.getUser(parseInt(userId)),
+ shopApi.getItems(),
+ ])
+ setUser(userData)
+ setItems(itemsData)
+ } catch (err) {
+ console.error('Failed to load data:', err)
+ toast.error('Ошибка загрузки данных')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleGrant = async () => {
+ if (!grantItem || !userId || !grantReason.trim()) return
+
+ setIsGranting(true)
+ try {
+ await shopApi.adminGrantItem(
+ parseInt(userId),
+ grantItem.id,
+ grantQuantity,
+ grantReason
+ )
+ toast.success(`Выдано ${grantItem.name} x${grantQuantity} для ${user?.nickname}`)
+ setGrantItem(null)
+ setGrantQuantity(1)
+ setGrantReason('')
+ } catch (err: unknown) {
+ const error = err as { response?: { data?: { detail?: string } } }
+ toast.error(error.response?.data?.detail || 'Ошибка выдачи предмета')
+ } finally {
+ setIsGranting(false)
+ }
+ }
+
+ const filteredItems = activeTab === 'all'
+ ? items
+ : items.filter(item => item.item_type === activeTab)
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (!user) {
+ return (
+
+
Пользователь не найден
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Выдать предмет
+
+
+ Получатель: {user.nickname}
+
+
+
+
+ {/* Tabs */}
+
+ {(Object.keys(ITEM_TYPE_LABELS) as (ShopItemType | 'all')[]).map((type) => (
+
+ ))}
+
+
+ {/* Items grid */}
+
+ {filteredItems.map(item => (
+
+ ))}
+
+
+ {filteredItems.length === 0 && (
+
+
+
Нет предметов в этой категории
+
+ )}
+
+ {/* Grant Modal */}
+ {grantItem && (
+
+
+
+
+
+ Выдать {grantItem.name}
+
+
+
+
+
+ Получатель: {user.nickname}
+
+
+ {/* Quantity */}
+
+
+
+
+
setGrantQuantity(Math.max(1, Math.min(100, parseInt(e.target.value) || 1)))}
+ className="w-20 text-center bg-dark-700/50 border border-dark-600 rounded-xl px-3 py-2 text-white font-bold text-lg focus:outline-none focus:border-neon-500/50"
+ />
+
+
+
+
+ {/* Reason */}
+
+
+
+
+
+ {
+ setGrantItem(null)
+ setGrantQuantity(1)
+ setGrantReason('')
+ }}
+ >
+ Отмена
+
+ }
+ >
+ Выдать x{grantQuantity}
+
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/admin/AdminUsersPage.tsx b/frontend/src/pages/admin/AdminUsersPage.tsx
index 56c3a04..9981832 100644
--- a/frontend/src/pages/admin/AdminUsersPage.tsx
+++ b/frontend/src/pages/admin/AdminUsersPage.tsx
@@ -1,10 +1,11 @@
import { useState, useEffect, useCallback } from 'react'
+import { Link } from 'react-router-dom'
import { adminApi } from '@/api'
import type { AdminUser, UserRole } from '@/types'
import { useToast } from '@/store/toast'
import { useConfirm } from '@/store/confirm'
import { NeonButton } from '@/components/ui'
-import { Search, Ban, UserCheck, Shield, ShieldOff, ChevronLeft, ChevronRight, Users, X, KeyRound, Bell, BellOff } from 'lucide-react'
+import { Search, Ban, UserCheck, Shield, ShieldOff, ChevronLeft, ChevronRight, Users, X, KeyRound, Bell, BellOff, Gift } from 'lucide-react'
export function AdminUsersPage() {
const [users, setUsers] = useState([])
@@ -319,6 +320,14 @@ export function AdminUsersPage() {
>
+
+
+
+
@@ -512,6 +521,7 @@ export function AdminUsersPage() {