('users')
+
+ const renderContent = () => {
+ switch (activeSection) {
+ case 'users':
+ return
+ case 'ui-kit':
+ return
+ case 'settings':
+ return (
+
+
Настройки
+
+
Раздел настроек в разработке
+
+
+ )
+ default:
+ return
+ }
+ }
+
+ return (
+
+
+
+ {renderContent()}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/admin-guard.tsx b/src/components/admin/admin-guard.tsx
new file mode 100644
index 0000000..0c9aa3d
--- /dev/null
+++ b/src/components/admin/admin-guard.tsx
@@ -0,0 +1,65 @@
+"use client"
+
+import { useState, useEffect, useRef } from 'react'
+import { useAdminAuth } from '@/hooks/useAdminAuth'
+import { AdminLogin } from './admin-login'
+
+interface AdminGuardProps {
+ children: React.ReactNode
+ fallback?: React.ReactNode
+}
+
+export function AdminGuard({ children, fallback }: AdminGuardProps) {
+ const { isAuthenticated, isLoading, checkAuth, admin } = useAdminAuth()
+ const [isChecking, setIsChecking] = useState(true)
+ const initRef = useRef(false) // Защита от повторных инициализаций
+
+ useEffect(() => {
+ const initAuth = async () => {
+ if (initRef.current) {
+ console.log('AdminGuard - Already initialized, skipping')
+ return
+ }
+
+ initRef.current = true
+ console.log('AdminGuard - Initializing admin auth check')
+ await checkAuth()
+ setIsChecking(false)
+ console.log('AdminGuard - Admin auth check completed, authenticated:', isAuthenticated, 'admin:', !!admin)
+ }
+
+ initAuth()
+ }, [checkAuth, isAuthenticated, admin])
+
+ // Дополнительное логирование состояний
+ useEffect(() => {
+ console.log('AdminGuard - State update:', {
+ isChecking,
+ isLoading,
+ isAuthenticated,
+ hasAdmin: !!admin
+ })
+ }, [isChecking, isLoading, isAuthenticated, admin])
+
+ // Показываем лоадер пока проверяем авторизацию
+ if (isChecking || isLoading) {
+ return (
+
+
+
+
Проверяем авторизацию администратора...
+
+
+ )
+ }
+
+ // Если не авторизован, показываем форму авторизации
+ if (!isAuthenticated) {
+ console.log('AdminGuard - Admin not authenticated, showing admin login')
+ return fallback ||
+ }
+
+ // Если авторизован, показываем защищенный контент
+ console.log('AdminGuard - Admin authenticated, showing admin panel')
+ return <>{children}>
+}
\ No newline at end of file
diff --git a/src/components/admin/admin-login.tsx b/src/components/admin/admin-login.tsx
new file mode 100644
index 0000000..4cd69bd
--- /dev/null
+++ b/src/components/admin/admin-login.tsx
@@ -0,0 +1,117 @@
+"use client"
+
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { useAdminAuth } from '@/hooks/useAdminAuth'
+import { Eye, EyeOff, Shield, Loader2 } from 'lucide-react'
+import { toast } from 'sonner'
+
+export function AdminLogin() {
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
+ const { login, isLoading } = useAdminAuth()
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ if (!username.trim() || !password.trim()) {
+ toast.error('Заполните все поля')
+ return
+ }
+
+ const result = await login(username.trim(), password)
+
+ if (!result.success) {
+ toast.error(result.message)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ Админ-панель
+
+
+ Вход в систему администрирования
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/admin-sidebar.tsx b/src/components/admin/admin-sidebar.tsx
new file mode 100644
index 0000000..f6854e3
--- /dev/null
+++ b/src/components/admin/admin-sidebar.tsx
@@ -0,0 +1,118 @@
+"use client"
+
+import { useAdminAuth } from '@/hooks/useAdminAuth'
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import { Avatar, AvatarFallback } from '@/components/ui/avatar'
+import {
+ Settings,
+ LogOut,
+ Users,
+ Shield,
+ Palette
+} from 'lucide-react'
+
+interface AdminSidebarProps {
+ activeSection: string
+ onSectionChange: (section: 'users' | 'ui-kit' | 'settings') => void
+}
+
+export function AdminSidebar({ activeSection, onSectionChange }: AdminSidebarProps) {
+ const { admin, logout } = useAdminAuth()
+
+ const getInitials = () => {
+ if (admin?.username) {
+ return admin.username.charAt(0).toUpperCase()
+ }
+ return 'A'
+ }
+
+ const handleLogout = () => {
+ logout()
+ }
+
+ return (
+
+ {/* Профиль администратора */}
+
+
+
+
+ {getInitials()}
+
+
+
+
+ {admin?.username || 'Администратор'}
+
+
+ Админ-панель
+
+
+
+
+
+ {/* Навигация */}
+
+ onSectionChange('users')}
+ >
+
+ Пользователи
+
+
+ onSectionChange('ui-kit')}
+ >
+
+ UI Kit
+
+
+ onSectionChange('settings')}
+ >
+
+ Настройки
+
+
+
+ {/* Управление */}
+
+
+
+ Выйти
+
+
+
+ {/* Логотип внизу */}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/ui-kit-section.tsx b/src/components/admin/ui-kit-section.tsx
new file mode 100644
index 0000000..6022520
--- /dev/null
+++ b/src/components/admin/ui-kit-section.tsx
@@ -0,0 +1,86 @@
+"use client"
+
+import { useState } from 'react'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { ButtonsDemo } from './ui-kit/buttons-demo'
+import { FormsDemo } from './ui-kit/forms-demo'
+import { CardsDemo } from './ui-kit/cards-demo'
+import { TypographyDemo } from './ui-kit/typography-demo'
+import { ColorsDemo } from './ui-kit/colors-demo'
+import { IconsDemo } from './ui-kit/icons-demo'
+
+export function UIKitSection() {
+ return (
+
+
+
UI Kit
+
Полная коллекция компонентов дизайн-системы SferaV
+
+
+
+
+
+ Кнопки
+
+
+ Формы
+
+
+ Карточки
+
+
+ Типографика
+
+
+ Цвета
+
+
+ Иконки
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/ui-kit/buttons-demo.tsx b/src/components/admin/ui-kit/buttons-demo.tsx
new file mode 100644
index 0000000..f5e2828
--- /dev/null
+++ b/src/components/admin/ui-kit/buttons-demo.tsx
@@ -0,0 +1,288 @@
+"use client"
+
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import {
+ Play,
+ Download,
+ Heart,
+ Settings,
+ Trash2,
+ Plus,
+ Search,
+ Filter,
+ Loader2
+} from 'lucide-react'
+
+export function ButtonsDemo() {
+ return (
+
+ {/* Основные варианты */}
+
+
+ Основные варианты
+
+
+ {/* Default */}
+
+
Default
+
+ Обычная кнопка
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="default"
+
+
+
+ {/* Glass */}
+
+
Glass (Основная)
+
+ Glass кнопка
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="glass"
+
+
+
+ {/* Glass Secondary */}
+
+
Glass Secondary
+
+ Вторичная glass
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="glass-secondary"
+
+
+
+ {/* Secondary */}
+
+
Secondary
+
+ Вторичная
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="secondary"
+
+
+
+ {/* Outline */}
+
+
Outline
+
+ Обводка
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="outline"
+
+
+
+ {/* Ghost */}
+
+
Ghost
+
+ Призрачная
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="ghost"
+
+
+
+ {/* Destructive */}
+
+
Destructive
+
+ Удалить
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="destructive"
+
+
+
+ {/* Link */}
+
+
Link
+
+ Ссылка
+ Маленькая
+ Большая
+ Отключена
+
+
+ variant="link"
+
+
+
+
+
+ {/* Кнопки с иконками */}
+
+
+ Кнопки с иконками
+
+
+ {/* Иконка слева */}
+
+
Иконка слева
+
+
+
+ Воспроизвести
+
+
+
+ Скачать
+
+
+
+ Поиск
+
+
+
+ Удалить
+
+
+
+
+ {/* Иконка справа */}
+
+
Иконка справа
+
+
+ Настройки
+
+
+
+ Фильтры
+
+
+
+
+
+ {/* Только иконка */}
+
+
Только иконка
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Загрузка */}
+
+
Состояние загрузки
+
+
+
+ Загрузка...
+
+
+
+ Сохранение...
+
+
+
+
+
+
+ {/* Размеры */}
+
+
+ Размеры
+
+
+
+
+ Маленькая (sm)
+
+
+ Обычная (default)
+
+
+ Большая (lg)
+
+
+
+ size="sm" | "default" | "lg" | "icon"
+
+
+
+
+ {/* Примеры использования */}
+
+
+ Примеры использования
+
+
+ {/* Группа действий */}
+
+
Группа действий
+
+
+
+ Создать
+
+
+ Редактировать
+
+
+
+ Удалить
+
+
+
+
+ {/* Навигация */}
+
+
Навигация
+
+ Назад
+ Продолжить
+
+
+
+ {/* Формы */}
+
+
Формы
+
+ Отмена
+ Сохранить
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/ui-kit/cards-demo.tsx b/src/components/admin/ui-kit/cards-demo.tsx
new file mode 100644
index 0000000..beb63c7
--- /dev/null
+++ b/src/components/admin/ui-kit/cards-demo.tsx
@@ -0,0 +1,413 @@
+"use client"
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Progress } from '@/components/ui/progress'
+import {
+ Heart,
+ Share2,
+ MoreHorizontal,
+ Star,
+ Clock,
+ MapPin,
+ Users,
+ TrendingUp,
+ Package,
+ Building,
+ Phone,
+ Calendar
+} from 'lucide-react'
+
+export function CardsDemo() {
+ return (
+
+ {/* Базовые карточки */}
+
+
+ Базовые карточки
+
+ Основные варианты карточек с Glass Morphism эффектом
+
+
+
+ {/* Простая карточка */}
+
+
Простая карточка
+
+
+ Заголовок карточки
+
+ Описание содержимого карточки
+
+
+
+
+ Основное содержимое карточки. Здесь может быть любая информация.
+
+
+
+
+
+ {/* Карточка с действиями */}
+
+
Карточка с действиями
+
+
+ Карточка с кнопками
+
+ Пример карточки с различными действиями
+
+
+
+
+ Содержимое карточки с возможностями взаимодействия.
+
+
+
+ Основное действие
+
+
+ Вторичное
+
+
+
+
+
+
+ {/* Интерактивная карточка */}
+
+
Интерактивная карточка
+
+
+
+
+ Кликабельная карточка
+
+
+
+
+
+
+ Карточка с hover эффектами
+
+
+
+
+ При наведении карточка меняет внешний вид и становится более яркой.
+
+
+
+
+
+
+
+ {/* Карточки продуктов */}
+
+
+ Карточки продуктов
+
+
+
+ {/* Карточка товара */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Новинка
+
+
+
+
+
+ Название товара
+
+
+
+ {[1, 2, 3, 4, 5].map((star) => (
+
+ ))}
+
+
(24 отзыва)
+
+
+
+
+ 1 299 ₽
+ 1 599 ₽
+
+
+
+ В корзину
+
+
+
+
+ {/* Карточка услуги */}
+
+
+
+
+
+ Фулфилмент услуги
+
+
+ Полный цикл обработки заказов
+
+
+
+ Активно
+
+
+
+
+
+
+
+ Москва, складской комплекс
+
+
+
+ 24/7 обработка заказов
+
+
+
+ до 1000 заказов/день
+
+
+
+
+ Загрузка склада
+ 75%
+
+
+
+
+ Подробнее
+
+
+
+
+ {/* Карточка статистики */}
+
+
+
+
+ Продажи за месяц
+
+
+
+
+
+
+
+ ₽2,847,532
+
+
+
+ +12.5%
+ от прошлого месяца
+
+
+
+
+ Заказов
+ 1,247
+
+
+ Средний чек
+ ₽2,284
+
+
+ Конверсия
+ 3.2%
+
+
+
+
+
+
+
+
+ {/* Карточки пользователей */}
+
+
+ Карточки пользователей
+
+
+
+ {/* Профиль пользователя */}
+
+
+
+
+
+ ИП
+
+
+
+
+ Иван Петров
+
+
+ Менеджер фулфилмент центра
+
+
+
+
+
+
+
+
+
+ ООО "Логистик Центр"
+
+
+
+ Регистрация: 15.03.2024
+
+
+
+
+ Связаться
+
+
+ Профиль
+
+
+
+
+
+ {/* Компактная карточка организации */}
+
+
+
+
+
+ ООО
+
+
+
+
+
+ Торговая компания "Альфа"
+
+
+ Селлер
+
+
+
+ ИНН: 7708123456789
+
+
+
+ Последний вход: 2 часа назад
+
+
+
+
+
+
+
+
+
+
+
+ {/* Специальные контейнеры */}
+
+
+ Специальные контейнеры
+
+
+ {/* Alert карточка */}
+
+
Уведомления
+
+
+
+
+
+
+
Успешно
+
Данные сохранены успешно
+
+
+
+
+
+
+
+
+
+
+
Предупреждение
+
Проверьте корректность введенных данных
+
+
+
+
+
+
+
+
+
+
+
Ошибка
+
Не удалось выполнить операцию
+
+
+
+
+
+
+
+ {/* Карточка с навигацией */}
+
+
Навигационная карточка
+
+
+ Быстрые действия
+
+
+
+
+
+ Товары
+
+
+
+ Клиенты
+
+
+
+ Аналитика
+
+
+
+ Склады
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/ui-kit/colors-demo.tsx b/src/components/admin/ui-kit/colors-demo.tsx
new file mode 100644
index 0000000..f4ea222
--- /dev/null
+++ b/src/components/admin/ui-kit/colors-demo.tsx
@@ -0,0 +1,367 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+
+export function ColorsDemo() {
+ return (
+
+ {/* Основная палитра */}
+
+
+ Основная цветовая палитра
+
+
+ {/* Primary цвета */}
+
+
Primary (Основной)
+
+
+
+
primary
+
oklch(0.65 0.28 315)
+
+
+
+
primary-foreground
+
oklch(0.985 0 0)
+
+
+
+
+ {/* Secondary цвета */}
+
+
Secondary (Вторичный)
+
+
+
+
secondary
+
oklch(0.94 0.08 315)
+
+
+
+
secondary-foreground
+
oklch(0.205 0 0)
+
+
+
+
+ {/* Accent цвета */}
+
+
Accent (Акцентный)
+
+
+
+
accent
+
oklch(0.90 0.12 315)
+
+
+
+
accent-foreground
+
oklch(0.205 0 0)
+
+
+
+
+ {/* Destructive цвета */}
+
+
Destructive (Деструктивный)
+
+
+
+
destructive
+
oklch(0.577 0.245 27.325)
+
+
+
+
+
+
+ {/* Нейтральные цвета */}
+
+
+ Нейтральные цвета
+
+
+
+ {/* Background цвета */}
+
+
Background
+
+
+
+
+
background
+
oklch(0.98 0.02 320)
+
+
+
+
+
+
foreground
+
oklch(0.145 0 0)
+
+
+
+
+
+ {/* Muted цвета */}
+
+
Muted
+
+
+
+
+
muted
+
oklch(0.94 0.05 315)
+
+
+
+
+
+
muted-foreground
+
oklch(0.556 0 0)
+
+
+
+
+
+
+ {/* Border и input цвета */}
+
+
+
Border & Input
+
+
+
+
+
border
+
oklch(0.90 0.08 315)
+
+
+
+
+
+
input
+
oklch(0.96 0.05 315)
+
+
+
+
+
+
ring
+
oklch(0.65 0.28 315)
+
+
+
+
+
+
+
Card & Popover
+
+
+
+
+
+
popover
+
oklch(1 0 0)
+
+
+
+
+
+
+
+
+ {/* Градиенты */}
+
+
+ Градиенты
+
+
+
+ {/* Основные градиенты */}
+
+
Основные градиенты
+
+
+
+
gradient-purple
+
Основной фиолетовый градиент
+
+
+
+
+
gradient-purple-light
+
Светлый фиолетовый градиент
+
+
+
+
+
bg-gradient-smooth
+
Темный градиент для фона
+
+
+
+
+ {/* Текстовые градиенты */}
+
+
Текстовые градиенты
+
+
+
+ Текст с ярким градиентом
+
+
text-gradient-bright
+
+
+
+
+ Текст с обычным градиентом
+
+
text-gradient
+
+
+
+
+ Текст с свечением
+
+
glow-text
+
+
+
+
+
+
+
+ {/* Прозрачности белого */}
+
+
+ Прозрачности белого цвета
+
+
+
+ {[
+ { opacity: '100', class: 'text-white', bg: 'bg-white' },
+ { opacity: '90', class: 'text-white/90', bg: 'bg-white/90' },
+ { opacity: '80', class: 'text-white/80', bg: 'bg-white/80' },
+ { opacity: '70', class: 'text-white/70', bg: 'bg-white/70' },
+ { opacity: '60', class: 'text-white/60', bg: 'bg-white/60' },
+ { opacity: '50', class: 'text-white/50', bg: 'bg-white/50' },
+ { opacity: '40', class: 'text-white/40', bg: 'bg-white/40' },
+ { opacity: '30', class: 'text-white/30', bg: 'bg-white/30' },
+ { opacity: '20', class: 'text-white/20', bg: 'bg-white/20' },
+ { opacity: '10', class: 'text-white/10', bg: 'bg-white/10' },
+ ].map((item) => (
+
+
+
+ {item.opacity}%
+
+
+ {item.class}
+
+
+ ))}
+
+
+
+
+ {/* Статусные цвета */}
+
+
+ Статусные цвета
+
+
+
+
+
Успех
+
+
+
green-500
+
Успешная операция
+
+
+
+
+
Предупреждение
+
+
+
yellow-500
+
Требует внимания
+
+
+
+
+
Ошибка
+
+
+
red-500
+
Ошибка или проблема
+
+
+
+
+
Информация
+
+
+
blue-500
+
Информационное сообщение
+
+
+
+
+
+
+ {/* Glass эффекты */}
+
+
+ Glass Morphism эффекты
+
+
+
+
+
Основные Glass эффекты
+
+
+
glass-card
+
Основная стеклянная карточка
+
+
+
+
glass-input
+
Стеклянное поле ввода
+
+
+
+
glass-sidebar
+
Стеклянный сайдбар
+
+
+
+
+
+
Glass кнопки
+
+
+
glass-button
+
Основная стеклянная кнопка
+
+
+
+
glass-secondary
+
Вторичная стеклянная кнопка
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/ui-kit/forms-demo.tsx b/src/components/admin/ui-kit/forms-demo.tsx
new file mode 100644
index 0000000..ea6b067
--- /dev/null
+++ b/src/components/admin/ui-kit/forms-demo.tsx
@@ -0,0 +1,527 @@
+"use client"
+
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Checkbox } from '@/components/ui/checkbox'
+import { Switch } from '@/components/ui/switch'
+import { Slider } from '@/components/ui/slider'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { PhoneInput } from '@/components/ui/phone-input'
+import {
+ Eye,
+ EyeOff,
+ Search,
+ Calendar,
+ Mail,
+ User,
+ Lock
+} from 'lucide-react'
+
+export function FormsDemo() {
+ const [showPassword, setShowPassword] = useState(false)
+ const [switchValue, setSwitchValue] = useState(false)
+ const [sliderValue, setSliderValue] = useState([50])
+ const [checkboxValue, setCheckboxValue] = useState(false)
+
+ return (
+
+ {/* Базовые инпуты */}
+
+
+ Базовые поля ввода
+
+
+ {/* Обычный инпут */}
+
+
+ Обычное поле
+
+
+
+
+ {/* Инпут с иконкой */}
+
+
+ Поле с иконкой
+
+
+
+
+
+
+
+ {/* Пароль */}
+
+
+ Пароль
+
+
+
+
+ setShowPassword(!showPassword)}
+ >
+ {showPassword ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Email */}
+
+
+ {/* Телефон */}
+
+
+ {/* Дата */}
+
+
+ {/* Число */}
+
+
+ Число
+
+
+
+
+ {/* Отключенное поле */}
+
+
+ Отключенное поле
+
+
+
+
+
+
+ {/* Селекты и выпадающие списки */}
+
+
+ Селекты и выпадающие списки
+
+
+
+
+ Выберите опцию
+
+
+
+
+
+
+ Опция 1
+ Опция 2
+ Опция 3
+ Опция 4
+
+
+
+
+
+
+ Тип организации
+
+
+
+
+
+
+ Фулфилмент
+ Селлер
+ Логистика
+ Оптовик
+
+
+
+
+
+
+ {/* Чекбоксы и переключатели */}
+
+
+ Чекбоксы и переключатели
+
+
+ {/* Чекбоксы */}
+
+
Чекбоксы
+
+
+
+
+ Согласие с условиями
+
+
+
+
+
+ Получать уведомления
+
+
+
+
+
+ Отключенный чекбокс
+
+
+
+
+
+ {/* Переключатели */}
+
+
Переключатели (Switch)
+
+
+
+
+ Включить уведомления
+
+
+
+
+
+ Темная тема
+
+
+
+
+
+ Отключенный переключатель
+
+
+
+
+
+
+
+ {/* Слайдеры */}
+
+
+ Слайдеры
+
+
+
+
+ Значение: {sliderValue[0]}
+
+
+
+
+
+
+ Диапазон цен: 1000 - 5000 ₽
+
+
+
+
+
+
+ {/* Состояния полей */}
+
+
+ Состояния полей
+
+
+ {/* Обычное состояние */}
+
+
+ Обычное состояние
+
+
+
+
+ {/* Фокус */}
+
+
+ Состояние фокуса
+
+
+
+
+ {/* Ошибка */}
+
+
+ Состояние ошибки
+
+
+
Это поле обязательно для заполнения
+
+
+ {/* Успех */}
+
+
+ Состояние успеха
+
+
+
Данные сохранены успешно
+
+
+
+
+ {/* Примеры форм */}
+
+
+ Примеры форм
+
+
+ {/* Форма входа */}
+
+
Форма входа
+
+
+
+
+
+
+ Запомнить меня
+
+
+
+ Войти
+
+
+
+
+ {/* Форма регистрации */}
+
+
Форма регистрации
+
+
+
+ Имя
+
+
+
+
+
+ Фамилия
+
+
+
+
+
+ Email
+
+
+
+
+
+
+ Тип организации
+
+
+
+
+
+
+ Фулфилмент
+ Селлер
+ Логистика
+ Оптовик
+
+
+
+
+
+
+ Согласен с условиями использования
+
+
+
+
+ Отмена
+
+
+ Зарегистрироваться
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/ui-kit/icons-demo.tsx b/src/components/admin/ui-kit/icons-demo.tsx
new file mode 100644
index 0000000..0b82c0f
--- /dev/null
+++ b/src/components/admin/ui-kit/icons-demo.tsx
@@ -0,0 +1,543 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+ // Основные иконки
+ Home, Settings, User, Search, Bell, Heart, Share2, Download, Upload,
+ Edit, Trash2, Plus, Minus, X, Check, ChevronDown, ChevronUp, ChevronLeft,
+ ChevronRight, ArrowLeft, ArrowRight, ArrowUp, ArrowDown,
+
+ // Бизнес иконки
+ Building, Package, Truck, Users, Phone, Mail, Calendar, Clock, MapPin,
+ TrendingUp, TrendingDown, BarChart, PieChart, DollarSign, CreditCard,
+
+ // Интерфейс
+ Menu, MoreHorizontal, MoreVertical, Eye, EyeOff, Lock, Unlock, Key,
+ Shield, AlertCircle, AlertTriangle, Info, CheckCircle, XCircle,
+
+ // Файлы и медиа
+ File, FileText, Image, Video, Music, Camera, Paperclip, Link2,
+
+ // Действия
+ Play, Pause, Square, SkipForward, SkipBack, Volume2, VolumeX,
+
+ // Социальные
+ MessageCircle, Send, ThumbsUp, ThumbsDown, Star, Bookmark,
+
+ // Статус
+ Wifi, WifiOff, Battery, Zap, Globe, Monitor, Smartphone, Tablet
+} from 'lucide-react'
+
+const iconSections = [
+ {
+ title: 'Основные иконки',
+ icons: [
+ { icon: Home, name: 'Home' },
+ { icon: Settings, name: 'Settings' },
+ { icon: User, name: 'User' },
+ { icon: Search, name: 'Search' },
+ { icon: Bell, name: 'Bell' },
+ { icon: Heart, name: 'Heart' },
+ { icon: Share2, name: 'Share2' },
+ { icon: Download, name: 'Download' },
+ { icon: Upload, name: 'Upload' },
+ { icon: Edit, name: 'Edit' },
+ { icon: Trash2, name: 'Trash2' },
+ { icon: Plus, name: 'Plus' },
+ { icon: Minus, name: 'Minus' },
+ { icon: X, name: 'X' },
+ { icon: Check, name: 'Check' },
+ ]
+ },
+ {
+ title: 'Навигация',
+ icons: [
+ { icon: ChevronDown, name: 'ChevronDown' },
+ { icon: ChevronUp, name: 'ChevronUp' },
+ { icon: ChevronLeft, name: 'ChevronLeft' },
+ { icon: ChevronRight, name: 'ChevronRight' },
+ { icon: ArrowLeft, name: 'ArrowLeft' },
+ { icon: ArrowRight, name: 'ArrowRight' },
+ { icon: ArrowUp, name: 'ArrowUp' },
+ { icon: ArrowDown, name: 'ArrowDown' },
+ { icon: Menu, name: 'Menu' },
+ { icon: MoreHorizontal, name: 'MoreHorizontal' },
+ { icon: MoreVertical, name: 'MoreVertical' },
+ ]
+ },
+ {
+ title: 'Бизнес и коммерция',
+ icons: [
+ { icon: Building, name: 'Building' },
+ { icon: Package, name: 'Package' },
+ { icon: Truck, name: 'Truck' },
+ { icon: Users, name: 'Users' },
+ { icon: Phone, name: 'Phone' },
+ { icon: Mail, name: 'Mail' },
+ { icon: Calendar, name: 'Calendar' },
+ { icon: Clock, name: 'Clock' },
+ { icon: MapPin, name: 'MapPin' },
+ { icon: TrendingUp, name: 'TrendingUp' },
+ { icon: TrendingDown, name: 'TrendingDown' },
+ { icon: BarChart, name: 'BarChart' },
+ { icon: PieChart, name: 'PieChart' },
+ { icon: DollarSign, name: 'DollarSign' },
+ { icon: CreditCard, name: 'CreditCard' },
+ ]
+ },
+ {
+ title: 'Безопасность и статус',
+ icons: [
+ { icon: Eye, name: 'Eye' },
+ { icon: EyeOff, name: 'EyeOff' },
+ { icon: Lock, name: 'Lock' },
+ { icon: Unlock, name: 'Unlock' },
+ { icon: Key, name: 'Key' },
+ { icon: Shield, name: 'Shield' },
+ { icon: AlertCircle, name: 'AlertCircle' },
+ { icon: AlertTriangle, name: 'AlertTriangle' },
+ { icon: Info, name: 'Info' },
+ { icon: CheckCircle, name: 'CheckCircle' },
+ { icon: XCircle, name: 'XCircle' },
+ ]
+ },
+ {
+ title: 'Файлы и медиа',
+ icons: [
+ { icon: File, name: 'File' },
+ { icon: FileText, name: 'FileText' },
+ { icon: Image, name: 'Image' },
+ { icon: Video, name: 'Video' },
+ { icon: Music, name: 'Music' },
+ { icon: Camera, name: 'Camera' },
+ { icon: Paperclip, name: 'Paperclip' },
+ { icon: Link2, name: 'Link2' },
+ ]
+ },
+ {
+ title: 'Медиа контролы',
+ icons: [
+ { icon: Play, name: 'Play' },
+ { icon: Pause, name: 'Pause' },
+ { icon: Square, name: 'Square' },
+ { icon: SkipForward, name: 'SkipForward' },
+ { icon: SkipBack, name: 'SkipBack' },
+ { icon: Volume2, name: 'Volume2' },
+ { icon: VolumeX, name: 'VolumeX' },
+ ]
+ },
+ {
+ title: 'Коммуникации',
+ icons: [
+ { icon: MessageCircle, name: 'MessageCircle' },
+ { icon: Send, name: 'Send' },
+ { icon: ThumbsUp, name: 'ThumbsUp' },
+ { icon: ThumbsDown, name: 'ThumbsDown' },
+ { icon: Star, name: 'Star' },
+ { icon: Bookmark, name: 'Bookmark' },
+ ]
+ },
+ {
+ title: 'Устройства и подключения',
+ icons: [
+ { icon: Wifi, name: 'Wifi' },
+ { icon: WifiOff, name: 'WifiOff' },
+ { icon: Battery, name: 'Battery' },
+ { icon: Zap, name: 'Zap' },
+ { icon: Globe, name: 'Globe' },
+ { icon: Monitor, name: 'Monitor' },
+ { icon: Smartphone, name: 'Smartphone' },
+ { icon: Tablet, name: 'Tablet' },
+ ]
+ }
+]
+
+export function IconsDemo() {
+ return (
+
+ {/* Размеры иконок */}
+
+
+ Размеры иконок
+
+
+
+
+
+
+ {/* Цвета иконок */}
+
+
+ Цвета иконок
+
+
+
+
+
+
+ {/* Заполненные иконки */}
+
+
+ Заполненные иконки
+
+
+
+
+ Добавьте класс fill-[color]
для заливки иконки
+
+
+
+
+ {/* Аватары */}
+
+
+ Аватары
+
+
+ {/* Размеры аватаров */}
+
+
+ {/* Типы аватаров */}
+
+
Типы
+
+
+
+
+
+
+
+ GR
+
+
+
Градиент
+
+
+
+
+ {/* Статусы */}
+
+
+
+
+ {/* Бейджи */}
+
+
+ Бейджи
+
+
+
+
Варианты
+
+ Default
+ Secondary
+ Outline
+ Destructive
+
+
+
+
+
С иконками
+
+
+
+ Активно
+
+
+
+ Ожидание
+
+
+
+ Предупреждение
+
+
+
+ Ошибка
+
+
+
+
+
+
Статусы организаций
+
+
+ Фулфилмент
+
+
+ Селлер
+
+
+ Логистика
+
+
+ Оптовик
+
+
+
+
+
+
+ {/* Коллекция иконок */}
+ {iconSections.map((section) => (
+
+
+ {section.title}
+
+
+
+ {section.icons.map((iconData) => {
+ const IconComponent = iconData.icon
+ return (
+
+ )
+ })}
+
+
+
+ ))}
+
+ {/* Примеры использования */}
+
+
+ Примеры использования в интерфейсе
+
+
+ {/* Кнопки с иконками */}
+
+
Кнопки с иконками
+
+
+
+ Создать
+
+
+
+ Редактировать
+
+
+
+ Удалить
+
+
+
+
+
+
+
+
+
+
+ {/* Элементы интерфейса */}
+
+
Элементы интерфейса
+
+
+
+
+ ИП
+
+
+
Иван Петров
+
+
+ ООО "Логистик"
+
+
+
+
+
+
+ Активен
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/ui-kit/typography-demo.tsx b/src/components/admin/ui-kit/typography-demo.tsx
new file mode 100644
index 0000000..3da6d4c
--- /dev/null
+++ b/src/components/admin/ui-kit/typography-demo.tsx
@@ -0,0 +1,390 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+
+export function TypographyDemo() {
+ return (
+
+ {/* Заголовки */}
+
+
+ Заголовки
+
+
+
+
+
+ Заголовок H1
+
+
+ text-4xl font-bold
+
+
+
+
+
+ Заголовок H2
+
+
+ text-3xl font-bold
+
+
+
+
+
+ Заголовок H3
+
+
+ text-2xl font-semibold
+
+
+
+
+
+ Заголовок H4
+
+
+ text-xl font-semibold
+
+
+
+
+
+ Заголовок H5
+
+
+ text-lg font-medium
+
+
+
+
+
+ Заголовок H6
+
+
+ text-base font-medium
+
+
+
+
+
+
+ {/* Градиентные заголовки */}
+
+
+ Градиентные заголовки
+
+
+
+
+ Яркий градиентный заголовок
+
+
+ text-gradient-bright
+
+
+
+
+
+ Обычный градиентный заголовок
+
+
+ text-gradient
+
+
+
+
+
+ Заголовок с свечением
+
+
+ glow-text
+
+
+
+
+
+ {/* Основной текст */}
+
+
+ Основной текст
+
+
+
+
+ Обычный текст. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation.
+
+
+ text-white text-base
+
+
+
+
+
+ Текст с прозрачностью 90%. Lorem ipsum dolor sit amet, consectetur
+ adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ text-white/90
+
+
+
+
+
+ Текст с прозрачностью 70%. Lorem ipsum dolor sit amet, consectetur
+ adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ text-white/70
+
+
+
+
+
+ Вторичный текст с прозрачностью 60%. Lorem ipsum dolor sit amet, consectetur
+ adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ text-white/60
+
+
+
+
+
+ {/* Размеры текста */}
+
+
+ Размеры текста
+
+
+
+
Очень мелкий текст
+
text-xs
+
+
+
+
Мелкий текст
+
text-sm
+
+
+
+
Базовый текст
+
text-base
+
+
+
+
Большой текст
+
text-lg
+
+
+
+
Очень большой текст
+
text-xl
+
+
+
+
Огромный текст
+
text-2xl
+
+
+
+
+ {/* Насыщенность шрифта */}
+
+
+ Насыщенность шрифта
+
+
+
+
Легкий шрифт
+
font-light
+
+
+
+
Обычный шрифт
+
font-normal
+
+
+
+
Средний шрифт
+
font-medium
+
+
+
+
Полужирный шрифт
+
font-semibold
+
+
+
+
Жирный шрифт
+
font-bold
+
+
+
+
+ {/* Специальные стили */}
+
+
+ Специальные стили текста
+
+
+
+
+ Моноширинный шрифт для кода
+
+
font-mono
+
+
+
+
+ Курсивный текст
+
+
italic
+
+
+
+
+ Подчеркнутый текст
+
+
underline
+
+
+
+
+ Зачеркнутый текст
+
+
line-through
+
+
+
+
+ Заглавные буквы
+
+
uppercase
+
+
+
+
+ СТРОЧНЫЕ БУКВЫ
+
+
lowercase
+
+
+
+
+ первая буква заглавная
+
+
capitalize
+
+
+
+
+ {/* Выравнивание текста */}
+
+
+ Выравнивание текста
+
+
+
+
+ Выравнивание по левому краю. Lorem ipsum dolor sit amet consectetur.
+
+
text-left
+
+
+
+
+ Выравнивание по центру. Lorem ipsum dolor sit amet consectetur.
+
+
text-center
+
+
+
+
+ Выравнивание по правому краю. Lorem ipsum dolor sit amet consectetur.
+
+
text-right
+
+
+
+
+ Выравнивание по ширине. Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation.
+
+
text-justify
+
+
+
+
+ {/* Списки */}
+
+
+ Списки
+
+
+
+
Маркированный список
+
+ Первый пункт списка
+ Второй пункт списка
+ Третий пункт списка с длинным текстом, который может переноситься на несколько строк
+ Четвертый пункт
+
+
+
+
+
Нумерованный список
+
+ Первый шаг процесса
+ Второй шаг процесса
+ Третий шаг с подробным описанием того, что нужно сделать
+ Завершающий шаг
+
+
+
+
+
+ {/* Цитаты и код */}
+
+
+ Цитаты и код
+
+
+
+
Цитата
+
+ "Дизайн - это не то, как вещь выглядит. Дизайн - это то, как вещь работает."
+
+
+
+
+
+
Инлайн код
+
+ Используйте класс glass-card
для создания карточек с эффектом стекла.
+
+
+
+
+
Блок кода
+
+{`
+
+
+ Заголовок карточки
+
+
+
+ Содержимое карточки
+
+ `}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/admin/users-section.tsx b/src/components/admin/users-section.tsx
new file mode 100644
index 0000000..b061c83
--- /dev/null
+++ b/src/components/admin/users-section.tsx
@@ -0,0 +1,286 @@
+"use client"
+
+import { useState, useEffect } from 'react'
+import { useQuery } from '@apollo/client'
+import { gql } from '@apollo/client'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Badge } from '@/components/ui/badge'
+import { Search, Phone, Building, Calendar, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
+
+// GraphQL запрос для получения пользователей
+const ALL_USERS = gql`
+ query AllUsers($search: String, $limit: Int, $offset: Int) {
+ allUsers(search: $search, limit: $limit, offset: $offset) {
+ users {
+ id
+ phone
+ managerName
+ avatar
+ createdAt
+ updatedAt
+ organization {
+ id
+ inn
+ name
+ fullName
+ type
+ status
+ createdAt
+ }
+ }
+ total
+ hasMore
+ }
+ }
+`
+
+interface User {
+ id: string
+ phone: string
+ managerName?: string
+ avatar?: string
+ createdAt: string
+ updatedAt: string
+ organization?: {
+ id: string
+ inn: string
+ name?: string
+ fullName?: string
+ type: string
+ status?: string
+ createdAt: string
+ }
+}
+
+export function UsersSection() {
+ const [search, setSearch] = useState('')
+ const [currentPage, setCurrentPage] = useState(1)
+ const [searchQuery, setSearchQuery] = useState('')
+ const limit = 20
+
+ const { data, loading, error, refetch } = useQuery(ALL_USERS, {
+ variables: {
+ search: searchQuery || undefined,
+ limit,
+ offset: (currentPage - 1) * limit
+ },
+ fetchPolicy: 'cache-and-network'
+ })
+
+ // Обновляем запрос при изменении поиска с дебаунсом
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setSearchQuery(search)
+ setCurrentPage(1) // Сбрасываем на первую страницу при поиске
+ }, 500)
+
+ return () => clearTimeout(timer)
+ }, [search])
+
+ const users = data?.allUsers?.users || []
+ const total = data?.allUsers?.total || 0
+ const hasMore = data?.allUsers?.hasMore || false
+ const totalPages = Math.ceil(total / limit)
+
+ const getOrganizationTypeBadge = (type: string) => {
+ const typeMap = {
+ FULFILLMENT: { label: 'Фулфилмент', variant: 'default' as const },
+ SELLER: { label: 'Селлер', variant: 'secondary' as const },
+ LOGIST: { label: 'Логистика', variant: 'outline' as const },
+ WHOLESALE: { label: 'Оптовик', variant: 'destructive' as const }
+ }
+ return typeMap[type as keyof typeof typeMap] || { label: type, variant: 'outline' as const }
+ }
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ }
+
+ const getInitials = (name?: string, phone?: string) => {
+ if (name) {
+ return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
+ }
+ if (phone) {
+ return phone.slice(-2)
+ }
+ return 'У'
+ }
+
+ const handlePrevPage = () => {
+ if (currentPage > 1) {
+ setCurrentPage(currentPage - 1)
+ }
+ }
+
+ const handleNextPage = () => {
+ if (currentPage < totalPages) {
+ setCurrentPage(currentPage + 1)
+ }
+ }
+
+ if (error) {
+ return (
+
+
+
Ошибка загрузки пользователей: {error.message}
+
refetch()}
+ className="mt-4 glass-button"
+ >
+ Попробовать снова
+
+
+
+ )
+ }
+
+ return (
+
+
+
Пользователи
+
Управление пользователями системы
+
+
+ {/* Поиск и статистика */}
+
+
+
+ setSearch(e.target.value)}
+ className="pl-10 glass-input text-white placeholder:text-white/50"
+ />
+
+
+ Всего: {total}
+ {searchQuery && Найдено: {users.length} }
+
+
+
+ {/* Список пользователей */}
+ {loading ? (
+
+
+
Загрузка пользователей...
+
+ ) : users.length === 0 ? (
+
+
+ {searchQuery ? 'Пользователи не найдены' : 'Пользователи отсутствуют'}
+
+
+ ) : (
+
+ {users.map((user: User) => (
+
+
+
+ {/* Аватар */}
+
+
+
+ {getInitials(user.managerName, user.phone)}
+
+
+
+ {/* Основная информация */}
+
+
+
+
+ {user.managerName || 'Без имени'}
+
+
+
+
+
+
+ {formatDate(user.createdAt)}
+
+
+
+
+ {/* Организация */}
+ {user.organization && (
+
+
+
+
+
+
+ {user.organization.name || user.organization.fullName || 'Без названия'}
+
+
+ {getOrganizationTypeBadge(user.organization.type).label}
+
+
+
+ ИНН: {user.organization.inn}
+
+ {user.organization.status && (
+
+ Статус: {user.organization.status}
+
+ )}
+
+
+
+ Зарегистрировано: {formatDate(user.organization.createdAt)}
+
+
+
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Пагинация */}
+ {totalPages > 1 && (
+
+
+ Страница {currentPage} из {totalPages}
+
+
+
+
+ Назад
+
+
+ Вперед
+
+
+
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts
index bded43f..cabd456 100644
--- a/src/graphql/mutations.ts
+++ b/src/graphql/mutations.ts
@@ -991,4 +991,30 @@ export const UPDATE_EMPLOYEE_SCHEDULE = gql`
mutation UpdateEmployeeSchedule($input: UpdateScheduleInput!) {
updateEmployeeSchedule(input: $input)
}
+`
+
+// Админ мутации
+export const ADMIN_LOGIN = gql`
+ mutation AdminLogin($username: String!, $password: String!) {
+ adminLogin(username: $username, password: $password) {
+ success
+ message
+ token
+ admin {
+ id
+ username
+ email
+ isActive
+ lastLogin
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`
+
+export const ADMIN_LOGOUT = gql`
+ mutation AdminLogout {
+ adminLogout
+ }
`
\ No newline at end of file
diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts
index 061f6bc..3fdcf49 100644
--- a/src/graphql/queries.ts
+++ b/src/graphql/queries.ts
@@ -567,4 +567,45 @@ export const GET_EMPLOYEE_SCHEDULE = gql`
}
}
}
+`
+
+// Админ запросы
+export const ADMIN_ME = gql`
+ query AdminMe {
+ adminMe {
+ id
+ username
+ email
+ isActive
+ lastLogin
+ createdAt
+ updatedAt
+ }
+ }
+`
+
+export const ALL_USERS = gql`
+ query AllUsers($search: String, $limit: Int, $offset: Int) {
+ allUsers(search: $search, limit: $limit, offset: $offset) {
+ users {
+ id
+ phone
+ managerName
+ avatar
+ createdAt
+ updatedAt
+ organization {
+ id
+ inn
+ name
+ fullName
+ type
+ status
+ createdAt
+ }
+ }
+ total
+ hasMore
+ }
+ }
`
\ No newline at end of file
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts
index f95eb27..a44b54f 100644
--- a/src/graphql/resolvers.ts
+++ b/src/graphql/resolvers.ts
@@ -1,4 +1,5 @@
import jwt from 'jsonwebtoken'
+import bcrypt from 'bcryptjs'
import { GraphQLError } from 'graphql'
import { GraphQLScalarType, Kind } from 'graphql'
import { prisma } from '@/lib/prisma'
@@ -18,6 +19,10 @@ interface Context {
id: string
phone: string
}
+ admin?: {
+ id: string
+ username: string
+ }
}
interface CreateEmployeeInput {
@@ -3736,4 +3741,167 @@ const logisticsMutations = {
resolvers.Mutation = {
...resolvers.Mutation,
...logisticsMutations
+}
+
+// Админ резолверы
+const adminQueries = {
+ adminMe: async (_: unknown, __: unknown, context: Context) => {
+ if (!context.admin) {
+ throw new GraphQLError('Требуется авторизация администратора', {
+ extensions: { code: 'UNAUTHENTICATED' }
+ })
+ }
+
+ const admin = await prisma.admin.findUnique({
+ where: { id: context.admin.id }
+ })
+
+ if (!admin) {
+ throw new GraphQLError('Администратор не найден')
+ }
+
+ return admin
+ },
+
+ allUsers: async (_: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context) => {
+ if (!context.admin) {
+ throw new GraphQLError('Требуется авторизация администратора', {
+ extensions: { code: 'UNAUTHENTICATED' }
+ })
+ }
+
+ const limit = args.limit || 50
+ const offset = args.offset || 0
+
+ // Строим условие поиска
+ const whereCondition: Prisma.UserWhereInput = args.search
+ ? {
+ OR: [
+ { phone: { contains: args.search, mode: 'insensitive' } },
+ { managerName: { contains: args.search, mode: 'insensitive' } },
+ {
+ organization: {
+ OR: [
+ { name: { contains: args.search, mode: 'insensitive' } },
+ { fullName: { contains: args.search, mode: 'insensitive' } },
+ { inn: { contains: args.search, mode: 'insensitive' } }
+ ]
+ }
+ }
+ ]
+ }
+ : {}
+
+ // Получаем пользователей с пагинацией
+ const [users, total] = await Promise.all([
+ prisma.user.findMany({
+ where: whereCondition,
+ include: {
+ organization: true
+ },
+ take: limit,
+ skip: offset,
+ orderBy: { createdAt: 'desc' }
+ }),
+ prisma.user.count({
+ where: whereCondition
+ })
+ ])
+
+ return {
+ users,
+ total,
+ hasMore: offset + limit < total
+ }
+ }
+}
+
+const adminMutations = {
+ adminLogin: async (_: unknown, args: { username: string; password: string }) => {
+ try {
+ // Найти администратора
+ const admin = await prisma.admin.findUnique({
+ where: { username: args.username }
+ })
+
+ if (!admin) {
+ return {
+ success: false,
+ message: 'Неверные учетные данные'
+ }
+ }
+
+ // Проверить активность
+ if (!admin.isActive) {
+ return {
+ success: false,
+ message: 'Аккаунт заблокирован'
+ }
+ }
+
+ // Проверить пароль
+ const isPasswordValid = await bcrypt.compare(args.password, admin.password)
+
+ if (!isPasswordValid) {
+ return {
+ success: false,
+ message: 'Неверные учетные данные'
+ }
+ }
+
+ // Обновить время последнего входа
+ await prisma.admin.update({
+ where: { id: admin.id },
+ data: { lastLogin: new Date() }
+ })
+
+ // Создать токен
+ const token = jwt.sign(
+ {
+ adminId: admin.id,
+ username: admin.username,
+ type: 'admin'
+ },
+ process.env.JWT_SECRET!,
+ { expiresIn: '24h' }
+ )
+
+ return {
+ success: true,
+ message: 'Успешная авторизация',
+ token,
+ admin: {
+ ...admin,
+ password: undefined // Не возвращаем пароль
+ }
+ }
+ } catch (error) {
+ console.error('Admin login error:', error)
+ return {
+ success: false,
+ message: 'Ошибка авторизации'
+ }
+ }
+ },
+
+ adminLogout: async (_: unknown, __: unknown, context: Context) => {
+ if (!context.admin) {
+ throw new GraphQLError('Требуется авторизация администратора', {
+ extensions: { code: 'UNAUTHENTICATED' }
+ })
+ }
+
+ return true
+ }
+}
+
+// Добавляем админ запросы и мутации к основным резолверам
+resolvers.Query = {
+ ...resolvers.Query,
+ ...adminQueries
+}
+
+resolvers.Mutation = {
+ ...resolvers.Mutation,
+ ...adminMutations
}
\ No newline at end of file
diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts
index 71be7d2..2deaa16 100644
--- a/src/graphql/typedefs.ts
+++ b/src/graphql/typedefs.ts
@@ -53,6 +53,10 @@ export const typeDefs = gql`
# Табель сотрудника за месяц
employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]!
+
+ # Админ запросы
+ adminMe: Admin
+ allUsers(search: String, limit: Int, offset: Int): UsersResponse!
}
type Mutation {
@@ -128,6 +132,10 @@ export const typeDefs = gql`
updateEmployee(id: ID!, input: UpdateEmployeeInput!): EmployeeResponse!
deleteEmployee(id: ID!): Boolean!
updateEmployeeSchedule(input: UpdateScheduleInput!): Boolean!
+
+ # Админ мутации
+ adminLogin(username: String!, password: String!): AdminAuthResponse!
+ adminLogout: Boolean!
}
# Типы данных
@@ -644,4 +652,28 @@ export const typeDefs = gql`
# JSON скаляр
scalar JSON
+
+ # Админ типы
+ type Admin {
+ id: ID!
+ username: String!
+ email: String
+ isActive: Boolean!
+ lastLogin: String
+ createdAt: String!
+ updatedAt: String!
+ }
+
+ type AdminAuthResponse {
+ success: Boolean!
+ message: String!
+ token: String
+ admin: Admin
+ }
+
+ type UsersResponse {
+ users: [User!]!
+ total: Int!
+ hasMore: Boolean!
+ }
`
\ No newline at end of file
diff --git a/src/hooks/useAdminAuth.ts b/src/hooks/useAdminAuth.ts
new file mode 100644
index 0000000..5fb2365
--- /dev/null
+++ b/src/hooks/useAdminAuth.ts
@@ -0,0 +1,255 @@
+import { useMutation, useQuery } from '@apollo/client'
+import { useState, useEffect } from 'react'
+import { gql } from '@apollo/client'
+import { apolloClient } from '@/lib/apollo-client'
+
+// GraphQL мутации и запросы
+const ADMIN_LOGIN = gql`
+ mutation AdminLogin($username: String!, $password: String!) {
+ adminLogin(username: $username, password: $password) {
+ success
+ message
+ token
+ admin {
+ id
+ username
+ email
+ isActive
+ lastLogin
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`
+
+const ADMIN_ME = gql`
+ query AdminMe {
+ adminMe {
+ id
+ username
+ email
+ isActive
+ lastLogin
+ createdAt
+ updatedAt
+ }
+ }
+`
+
+const ADMIN_LOGOUT = gql`
+ mutation AdminLogout {
+ adminLogout
+ }
+`
+
+interface Admin {
+ id: string
+ username: string
+ email?: string
+ isActive: boolean
+ lastLogin?: string
+ createdAt: string
+ updatedAt: string
+}
+
+interface UseAdminAuthReturn {
+ login: (username: string, password: string) => Promise<{ success: boolean; message: string; admin?: Admin }>
+ logout: () => void
+ admin: Admin | null
+ isAuthenticated: boolean
+ isLoading: boolean
+ checkAuth: () => Promise
+}
+
+// Утилиты для работы с токенами администратора
+const ADMIN_TOKEN_KEY = 'adminAuthToken'
+const ADMIN_DATA_KEY = 'adminData'
+
+const setAdminToken = (token: string) => {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem(ADMIN_TOKEN_KEY, token)
+ }
+}
+
+const getAdminToken = (): string | null => {
+ if (typeof window !== 'undefined') {
+ return localStorage.getItem(ADMIN_TOKEN_KEY)
+ }
+ return null
+}
+
+const removeAdminToken = () => {
+ if (typeof window !== 'undefined') {
+ localStorage.removeItem(ADMIN_TOKEN_KEY)
+ localStorage.removeItem(ADMIN_DATA_KEY)
+ }
+}
+
+const setAdminData = (admin: Admin) => {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem(ADMIN_DATA_KEY, JSON.stringify(admin))
+ }
+}
+
+export const useAdminAuth = (): UseAdminAuthReturn => {
+ const [isLoading, setIsLoading] = useState(false)
+ const [admin, setAdmin] = useState(null)
+ const [isAuthenticated, setIsAuthenticated] = useState(() => {
+ return !!getAdminToken()
+ })
+ const [isCheckingAuth, setIsCheckingAuth] = useState(false)
+
+ const [adminLoginMutation] = useMutation(ADMIN_LOGIN)
+ const [adminLogoutMutation] = useMutation(ADMIN_LOGOUT)
+
+ // Проверка авторизации администратора
+ const checkAuth = async () => {
+ if (isCheckingAuth) {
+ return
+ }
+
+ const token = getAdminToken()
+ console.log('useAdminAuth - checkAuth called, token exists:', !!token)
+
+ if (!token) {
+ setIsAuthenticated(false)
+ setAdmin(null)
+ setIsCheckingAuth(false)
+ return
+ }
+
+ setIsCheckingAuth(true)
+
+ try {
+ console.log('useAdminAuth - Making ADMIN_ME query')
+ const { data } = await apolloClient.query({
+ query: ADMIN_ME,
+ errorPolicy: 'all',
+ fetchPolicy: 'network-only',
+ context: {
+ headers: {
+ authorization: `Bearer ${token}`
+ }
+ }
+ })
+
+ console.log('useAdminAuth - ADMIN_ME response:', !!data?.adminMe)
+ if (data?.adminMe) {
+ setAdmin(data.adminMe)
+ setIsAuthenticated(true)
+ setAdminData(data.adminMe)
+ console.log('useAdminAuth - Admin authenticated:', data.adminMe.username)
+ } else {
+ setIsAuthenticated(false)
+ setAdmin(null)
+ }
+ } catch (error: unknown) {
+ console.log('useAdminAuth - ADMIN_ME error:', error)
+ if ((error as { graphQLErrors?: Array<{ extensions?: { code?: string } }> })?.graphQLErrors?.some((e) => e.extensions?.code === 'UNAUTHENTICATED')) {
+ logout()
+ } else {
+ setIsAuthenticated(false)
+ setAdmin(null)
+ }
+ } finally {
+ setIsCheckingAuth(false)
+ }
+ }
+
+ // Проверяем авторизацию при загрузке компонента
+ useEffect(() => {
+ const token = getAdminToken()
+ console.log('useAdminAuth - useEffect init, token exists:', !!token, 'admin exists:', !!admin, 'isChecking:', isCheckingAuth)
+
+ if (token && !admin && !isCheckingAuth) {
+ console.log('useAdminAuth - Running checkAuth because token exists but no admin data')
+ checkAuth()
+ } else if (!token) {
+ console.log('useAdminAuth - No token, setting unauthenticated state')
+ setIsAuthenticated(false)
+ setAdmin(null)
+ }
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
+
+ const login = async (username: string, password: string) => {
+ try {
+ setIsLoading(true)
+ console.log('useAdminAuth - Starting adminLogin mutation')
+
+ const { data } = await adminLoginMutation({
+ variables: { username, password }
+ })
+
+ console.log('useAdminAuth - GraphQL response data:', data)
+ const result = data.adminLogin
+ console.log('useAdminAuth - Admin login result:', {
+ success: result.success,
+ hasToken: !!result.token,
+ hasAdmin: !!result.admin,
+ message: result.message
+ })
+
+ if (result.success && result.token && result.admin) {
+ // Сохраняем токен и данные администратора
+ console.log('useAdminAuth - Saving admin token')
+ setAdminToken(result.token)
+ setAdminData(result.admin)
+
+ // Обновляем состояние хука
+ setAdmin(result.admin)
+ setIsAuthenticated(true)
+ console.log('useAdminAuth - State updated: admin set, isAuthenticated=true')
+
+ // Принудительно обновляем Apollo Client
+ apolloClient.resetStore()
+
+ // Перенаправляем в админ-дашборд
+ if (typeof window !== 'undefined') {
+ window.location.href = '/admin/dashboard'
+ }
+
+ return {
+ success: true,
+ message: result.message,
+ admin: result.admin
+ }
+ }
+
+ return {
+ success: false,
+ message: result.message
+ }
+ } catch (error) {
+ console.error('Error during admin login:', error)
+ return {
+ success: false,
+ message: 'Ошибка при авторизации'
+ }
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const logout = () => {
+ console.log('useAdminAuth - Logging out')
+ removeAdminToken()
+ setAdmin(null)
+ setIsAuthenticated(false)
+ apolloClient.resetStore()
+
+ // Перенаправляем на страницу входа администратора
+ if (typeof window !== 'undefined') {
+ window.location.href = '/admin'
+ }
+ }
+
+ return {
+ login,
+ logout,
+ admin,
+ isAuthenticated,
+ isLoading,
+ checkAuth
+ }
+}
\ No newline at end of file
diff --git a/src/lib/apollo-client.ts b/src/lib/apollo-client.ts
index db91026..31677bb 100644
--- a/src/lib/apollo-client.ts
+++ b/src/lib/apollo-client.ts
@@ -9,10 +9,19 @@ const httpLink = createHttpLink({
// Auth Link для добавления JWT токена в заголовки
const authLink = setContext((operation, { headers }) => {
- // Получаем токен из localStorage каждый раз
- const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null
+ if (typeof window === 'undefined') {
+ return { headers }
+ }
- console.log(`Apollo Client - Operation: ${operation.operationName}, Token:`, token ? `${token.substring(0, 20)}...` : 'No token')
+ // Проверяем токены администратора и пользователя
+ const adminToken = localStorage.getItem('adminAuthToken')
+ const userToken = localStorage.getItem('authToken')
+
+ // Приоритет у админского токена
+ const token = adminToken || userToken
+ const tokenType = adminToken ? 'admin' : 'user'
+
+ console.log(`Apollo Client - Operation: ${operation.operationName}, Token type: ${tokenType}, Token:`, token ? `${token.substring(0, 20)}...` : 'No token')
const authHeaders = {
...headers,
@@ -35,14 +44,24 @@ const errorLink = onError(({ graphQLErrors, networkError }) => {
)
// Если токен недействителен, очищаем localStorage и перенаправляем на авторизацию
- // Но не делаем редирект если пользователь уже на главной странице (в процессе авторизации)
if (extensions?.code === 'UNAUTHENTICATED') {
if (typeof window !== 'undefined') {
- localStorage.removeItem('authToken')
- localStorage.removeItem('userData')
- // Перенаправляем на страницу авторизации только если не находимся на ней
- if (window.location.pathname !== '/') {
- window.location.href = '/'
+ const isAdminPath = window.location.pathname.startsWith('/admin')
+
+ if (isAdminPath) {
+ // Для админских страниц очищаем админские токены
+ localStorage.removeItem('adminAuthToken')
+ localStorage.removeItem('adminData')
+ if (window.location.pathname !== '/admin') {
+ window.location.href = '/admin'
+ }
+ } else {
+ // Для пользовательских страниц очищаем пользовательские токены
+ localStorage.removeItem('authToken')
+ localStorage.removeItem('userData')
+ if (window.location.pathname !== '/') {
+ window.location.href = '/'
+ }
}
}
}