Files
sfera-new/src/components/admin/users-section.tsx

294 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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) => {
try {
const date = new Date(dateString)
if (isNaN(date.getTime())) {
return 'Неизвестно'
}
return date.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
} catch (error) {
return 'Неизвестно'
}
}
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 (
<div className="p-8">
<div className="glass-card p-6 text-center">
<p className="text-red-400">Ошибка загрузки пользователей: {error.message}</p>
<Button
onClick={() => refetch()}
className="mt-4 glass-button"
>
Попробовать снова
</Button>
</div>
</div>
)
}
return (
<div className="p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Пользователи</h1>
<p className="text-white/70">Управление пользователями системы</p>
</div>
{/* Поиск и статистика */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/50 h-4 w-4" />
<Input
type="text"
placeholder="Поиск по телефону, имени, ИНН организации..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 glass-input text-white placeholder:text-white/50"
/>
</div>
<div className="flex items-center space-x-4 text-white/70 text-sm">
<span>Всего: {total}</span>
{searchQuery && <span>Найдено: {users.length}</span>}
</div>
</div>
{/* Список пользователей */}
{loading ? (
<div className="glass-card p-8 text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-white/70" />
<p className="text-white/70">Загрузка пользователей...</p>
</div>
) : users.length === 0 ? (
<div className="glass-card p-8 text-center">
<p className="text-white/70">
{searchQuery ? 'Пользователи не найдены' : 'Пользователи отсутствуют'}
</p>
</div>
) : (
<div className="space-y-4">
{users.map((user: User) => (
<Card key={user.id} className="glass-card border-white/10 hover:border-white/20 transition-all">
<CardContent className="p-6">
<div className="flex items-start space-x-4">
{/* Аватар */}
<Avatar className="h-12 w-12 bg-white/20">
<AvatarImage src={user.avatar} />
<AvatarFallback className="bg-white/20 text-white text-sm font-semibold">
{getInitials(user.managerName, user.phone)}
</AvatarFallback>
</Avatar>
{/* Основная информация */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="text-white font-medium">
{user.managerName || 'Без имени'}
</h3>
<div className="flex items-center text-white/70 text-sm mt-1">
<Phone className="h-3 w-3 mr-1" />
{user.phone}
</div>
</div>
<div className="text-right">
<div className="flex items-center text-white/70 text-sm">
<Calendar className="h-3 w-3 mr-1" />
{formatDate(user.createdAt)}
</div>
</div>
</div>
{/* Организация */}
{user.organization && (
<div className="bg-white/5 rounded-lg p-3 mt-3">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<Building className="h-4 w-4 text-white/70" />
<span className="text-white font-medium truncate">
{user.organization.name || user.organization.fullName || 'Без названия'}
</span>
<Badge {...getOrganizationTypeBadge(user.organization.type)}>
{getOrganizationTypeBadge(user.organization.type).label}
</Badge>
</div>
<p className="text-white/70 text-sm">
ИНН: {user.organization.inn}
</p>
{user.organization.status && (
<p className="text-white/60 text-xs">
Статус: {user.organization.status}
</p>
)}
</div>
<div className="text-right">
<p className="text-white/60 text-xs">
Зарегистрировано: {formatDate(user.organization.createdAt)}
</p>
</div>
</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* Пагинация */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-8 pt-6 border-t border-white/10">
<div className="text-white/70 text-sm">
Страница {currentPage} из {totalPages}
</div>
<div className="flex space-x-2">
<Button
onClick={handlePrevPage}
disabled={currentPage === 1}
variant="ghost"
className="glass-button text-white disabled:opacity-50"
>
<ChevronLeft className="h-4 w-4 mr-1" />
Назад
</Button>
<Button
onClick={handleNextPage}
disabled={currentPage === totalPages}
variant="ghost"
className="glass-button text-white disabled:opacity-50"
>
Вперед
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
)}
</div>
)
}