Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен README с описанием функционала и технологий. Реализована анимация и адаптивный дизайн. Настроена авторизация с использованием Apollo Client.

This commit is contained in:
Bivekich
2025-07-16 18:00:41 +03:00
parent d260749bc9
commit 823ef9a28c
69 changed files with 15539 additions and 210 deletions

View File

@ -0,0 +1,326 @@
"use client"
import { useState } from 'react'
import { useQuery, useMutation } from '@apollo/client'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Users,
Clock,
Send,
CheckCircle,
XCircle,
ArrowUpCircle,
ArrowDownCircle
} from 'lucide-react'
import { OrganizationCard } from './organization-card'
import { GET_MY_COUNTERPARTIES, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
import { RESPOND_TO_COUNTERPARTY_REQUEST, CANCEL_COUNTERPARTY_REQUEST, REMOVE_COUNTERPARTY } from '@/graphql/mutations'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
createdAt: string
users?: Array<{ id: string, avatar?: string }>
}
interface CounterpartyRequest {
id: string
message?: string
status: 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED'
createdAt: string
sender: Organization
receiver: Organization
}
export function MarketCounterparties() {
const { data: counterpartiesData, loading: counterpartiesLoading, refetch: refetchCounterparties } = useQuery(GET_MY_COUNTERPARTIES)
const { data: incomingData, loading: incomingLoading, refetch: refetchIncoming } = useQuery(GET_INCOMING_REQUESTS)
const { data: outgoingData, loading: outgoingLoading, refetch: refetchOutgoing } = useQuery(GET_OUTGOING_REQUESTS)
const [respondToRequest] = useMutation(RESPOND_TO_COUNTERPARTY_REQUEST, {
onCompleted: () => {
refetchIncoming()
refetchCounterparties()
}
})
const [cancelRequest] = useMutation(CANCEL_COUNTERPARTY_REQUEST, {
onCompleted: () => {
refetchOutgoing()
}
})
const [removeCounterparty] = useMutation(REMOVE_COUNTERPARTY, {
onCompleted: () => {
refetchCounterparties()
}
})
const handleAcceptRequest = async (requestId: string) => {
try {
await respondToRequest({
variables: { requestId, response: 'ACCEPTED' }
})
} catch (error) {
console.error('Ошибка при принятии заявки:', error)
}
}
const handleRejectRequest = async (requestId: string) => {
try {
await respondToRequest({
variables: { requestId, response: 'REJECTED' }
})
} catch (error) {
console.error('Ошибка при отклонении заявки:', error)
}
}
const handleCancelRequest = async (requestId: string) => {
try {
await cancelRequest({
variables: { requestId }
})
} catch (error) {
console.error('Ошибка при отмене заявки:', error)
}
}
const handleRemoveCounterparty = async (organizationId: string) => {
try {
await removeCounterparty({
variables: { organizationId }
})
} catch (error) {
console.error('Ошибка при удалении контрагента:', error)
}
}
const formatDate = (dateString: string) => {
if (!dateString) return ''
try {
let date: Date
// Проверяем, является ли строка числом (Unix timestamp)
if (/^\d+$/.test(dateString)) {
// Если это Unix timestamp в миллисекундах
const timestamp = parseInt(dateString, 10)
date = new Date(timestamp)
} else {
// Обычная строка даты
date = new Date(dateString)
}
if (isNaN(date.getTime())) {
return 'Неверная дата'
}
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
} catch (error) {
return 'Ошибка даты'
}
}
const counterparties = counterpartiesData?.myCounterparties || []
const incomingRequests = incomingData?.incomingRequests || []
const outgoingRequests = outgoingData?.outgoingRequests || []
return (
<div className="h-full flex flex-col">
<div className="flex items-center space-x-3 mb-6">
<Users className="h-6 w-6 text-blue-400" />
<div>
<h3 className="text-lg font-semibold text-white">Мои контрагенты</h3>
<p className="text-white/60 text-sm">Управление контрагентами и заявками</p>
</div>
</div>
<div className="flex-1 overflow-hidden">
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-3 bg-white/5 border-white/10">
<TabsTrigger value="counterparties" className="data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300">
<Users className="h-4 w-4 mr-2" />
Контрагенты ({counterparties.length})
</TabsTrigger>
<TabsTrigger value="incoming" className="data-[state=active]:bg-green-500/20 data-[state=active]:text-green-300">
<ArrowDownCircle className="h-4 w-4 mr-2" />
Входящие ({incomingRequests.length})
</TabsTrigger>
<TabsTrigger value="outgoing" className="data-[state=active]:bg-orange-500/20 data-[state=active]:text-orange-300">
<ArrowUpCircle className="h-4 w-4 mr-2" />
Исходящие ({outgoingRequests.length})
</TabsTrigger>
</TabsList>
<TabsContent value="counterparties" className="flex-1 overflow-auto mt-4">
{counterpartiesLoading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загрузка...</div>
</div>
) : counterparties.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">У вас пока нет контрагентов</p>
<p className="text-white/40 text-sm mt-2">
Перейдите на другие вкладки, чтобы найти партнеров
</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{counterparties.map((organization: Organization) => (
<OrganizationCard
key={organization.id}
organization={organization}
onRemove={handleRemoveCounterparty}
showRemoveButton={true}
/>
))}
</div>
)}
</TabsContent>
<TabsContent value="incoming" className="flex-1 overflow-auto mt-4">
{incomingLoading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загрузка...</div>
</div>
) : incomingRequests.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<ArrowDownCircle className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">Нет входящих заявок</p>
</div>
</div>
) : (
<div className="space-y-4">
{incomingRequests.map((request: CounterpartyRequest) => (
<Card key={request.id} className="glass-card p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
{(request.sender.name || request.sender.fullName || 'O').charAt(0).toUpperCase()}
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="text-white font-medium">
{request.sender.name || request.sender.fullName}
</h4>
<p className="text-white/60 text-sm">ИНН: {request.sender.inn}</p>
{request.message && (
<p className="text-white/80 text-sm mt-2 italic">&quot;{request.message}&quot;</p>
)}
<div className="flex items-center space-x-2 mt-2">
<Clock className="h-3 w-3 text-white/40" />
<span className="text-white/40 text-xs">{formatDate(request.createdAt)}</span>
</div>
</div>
</div>
<div className="flex space-x-2 ml-4">
<Button
size="sm"
onClick={() => handleAcceptRequest(request.id)}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30 cursor-pointer"
>
<CheckCircle className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleRejectRequest(request.id)}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer"
>
<XCircle className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
))}
</div>
)}
</TabsContent>
<TabsContent value="outgoing" className="flex-1 overflow-auto mt-4">
{outgoingLoading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загрузка...</div>
</div>
) : outgoingRequests.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<ArrowUpCircle className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">Нет исходящих заявок</p>
</div>
</div>
) : (
<div className="space-y-4">
{outgoingRequests.map((request: CounterpartyRequest) => (
<Card key={request.id} className="glass-card p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
{(request.receiver.name || request.receiver.fullName || 'O').charAt(0).toUpperCase()}
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="text-white font-medium">
{request.receiver.name || request.receiver.fullName}
</h4>
<p className="text-white/60 text-sm">ИНН: {request.receiver.inn}</p>
{request.message && (
<p className="text-white/80 text-sm mt-2 italic">&quot;{request.message}&quot;</p>
)}
<div className="flex items-center space-x-4 mt-2">
<div className="flex items-center space-x-2">
<Clock className="h-3 w-3 text-white/40" />
<span className="text-white/40 text-xs">{formatDate(request.createdAt)}</span>
</div>
<Badge className={
request.status === 'PENDING' ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' :
request.status === 'REJECTED' ? 'bg-red-500/20 text-red-300 border-red-500/30' :
'bg-gray-500/20 text-gray-300 border-gray-500/30'
}>
{request.status === 'PENDING' ? 'Ожидает' : request.status === 'REJECTED' ? 'Отклонено' : request.status}
</Badge>
</div>
</div>
</div>
{request.status === 'PENDING' && (
<Button
size="sm"
variant="outline"
onClick={() => handleCancelRequest(request.id)}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer ml-4"
>
<XCircle className="h-4 w-4" />
</Button>
)}
</div>
</Card>
))}
</div>
)}
</TabsContent>
</Tabs>
</div>
</div>
)
}

View File

@ -0,0 +1,97 @@
"use client"
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Card } from '@/components/ui/card'
import { Sidebar } from '@/components/dashboard/sidebar'
import { MarketCounterparties } from './market-counterparties'
import { MarketFulfillment } from './market-fulfillment'
import { MarketSellers } from './market-sellers'
import { MarketLogistics } from './market-logistics'
import { MarketWholesale } from './market-wholesale'
export function MarketDashboard() {
return (
<div className="h-screen bg-gradient-smooth flex overflow-hidden">
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<div className="h-full w-full flex flex-col">
{/* Заголовок - фиксированная высота */}
<div className="flex items-center justify-between mb-4 flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Маркет</h1>
<p className="text-white/70 text-sm">Управление контрагентами и поиск партнеров</p>
</div>
</div>
{/* Основной контент с табами */}
<div className="flex-1 overflow-hidden">
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-5 bg-white/5 backdrop-blur border-white/10 flex-shrink-0">
<TabsTrigger
value="counterparties"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
>
Мои контрагенты
</TabsTrigger>
<TabsTrigger
value="fulfillment"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
>
Фулфилмент
</TabsTrigger>
<TabsTrigger
value="sellers"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
>
Селлеры
</TabsTrigger>
<TabsTrigger
value="logistics"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
>
Логистика
</TabsTrigger>
<TabsTrigger
value="wholesale"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
>
Оптовик
</TabsTrigger>
</TabsList>
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-6">
<Card className="glass-card h-full overflow-hidden p-6">
<MarketCounterparties />
</Card>
</TabsContent>
<TabsContent value="fulfillment" className="flex-1 overflow-hidden mt-6">
<Card className="glass-card h-full overflow-hidden p-6">
<MarketFulfillment />
</Card>
</TabsContent>
<TabsContent value="sellers" className="flex-1 overflow-hidden mt-6">
<Card className="glass-card h-full overflow-hidden p-6">
<MarketSellers />
</Card>
</TabsContent>
<TabsContent value="logistics" className="flex-1 overflow-hidden mt-6">
<Card className="glass-card h-full overflow-hidden p-6">
<MarketLogistics />
</Card>
</TabsContent>
<TabsContent value="wholesale" className="flex-1 overflow-hidden mt-6">
<Card className="glass-card h-full overflow-hidden p-6">
<MarketWholesale />
</Card>
</TabsContent>
</Tabs>
</div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,125 @@
"use client"
import { useState } from 'react'
import { useQuery, useMutation } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Search, Package } from 'lucide-react'
import { OrganizationCard } from './organization-card'
import { SEARCH_ORGANIZATIONS } from '@/graphql/queries'
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
createdAt: string
users?: Array<{ id: string, avatar?: string }>
isCounterparty?: boolean
}
export function MarketFulfillment() {
const [searchTerm, setSearchTerm] = useState('')
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
variables: { type: 'FULFILLMENT', search: searchTerm || null }
})
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
onCompleted: () => {
refetch()
}
})
const handleSearch = () => {
refetch({ type: 'FULFILLMENT', search: searchTerm || null })
}
const handleSendRequest = async (organizationId: string, message: string) => {
try {
await sendRequest({
variables: {
receiverId: organizationId,
message: message || 'Заявка на добавление в контрагенты'
}
})
} catch (error) {
console.error('Ошибка отправки заявки:', error)
}
}
const organizations = data?.searchOrganizations || []
return (
<div className="h-full flex flex-col space-y-4 overflow-hidden">
{/* Поиск */}
<div className="flex space-x-4 flex-shrink-0">
<div className="relative flex-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск фулфилментов по названию или ИНН..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<Button
onClick={handleSearch}
className="bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30 cursor-pointer"
>
<Search className="h-4 w-4 mr-2" />
Найти
</Button>
</div>
{/* Заголовок с иконкой */}
<div className="flex items-center space-x-3 flex-shrink-0 mb-4">
<Package className="h-6 w-6 text-blue-400" />
<div>
<h3 className="text-lg font-semibold text-white">Фулфилмент-центры</h3>
<p className="text-white/60 text-sm">Найдите и добавьте фулфилмент-центры в контрагенты</p>
</div>
</div>
{/* Результаты поиска */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Поиск...</div>
</div>
) : organizations.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<Package className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{searchTerm ? 'Фулфилмент-центры не найдены' : 'Введите запрос для поиска фулфилментов'}
</p>
<p className="text-white/40 text-sm mt-2">
Попробуйте изменить условия поиска
</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{organizations.map((organization: Organization) => (
<OrganizationCard
key={organization.id}
organization={organization}
onSendRequest={handleSendRequest}
actionButtonText="Добавить"
actionButtonColor="blue"
requestSending={sendingRequest}
/>
))}
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,125 @@
"use client"
import { useState } from 'react'
import { useQuery, useMutation } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Search, Truck } from 'lucide-react'
import { OrganizationCard } from './organization-card'
import { SEARCH_ORGANIZATIONS } from '@/graphql/queries'
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
createdAt: string
users?: Array<{ id: string, avatar?: string }>
isCounterparty?: boolean
}
export function MarketLogistics() {
const [searchTerm, setSearchTerm] = useState('')
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
variables: { type: 'LOGIST', search: searchTerm || null }
})
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
onCompleted: () => {
refetch()
}
})
const handleSearch = () => {
refetch({ type: 'LOGIST', search: searchTerm || null })
}
const handleSendRequest = async (organizationId: string, message: string) => {
try {
await sendRequest({
variables: {
receiverId: organizationId,
message: message || 'Заявка на добавление в контрагенты'
}
})
} catch (error) {
console.error('Ошибка отправки заявки:', error)
}
}
const organizations = data?.searchOrganizations || []
return (
<div className="h-full flex flex-col space-y-4 overflow-hidden">
{/* Поиск */}
<div className="flex space-x-4 flex-shrink-0">
<div className="relative flex-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск логистических компаний по названию или ИНН..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<Button
onClick={handleSearch}
className="bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border-orange-500/30 cursor-pointer"
>
<Search className="h-4 w-4 mr-2" />
Найти
</Button>
</div>
{/* Заголовок с иконкой */}
<div className="flex items-center space-x-3 flex-shrink-0 mb-4">
<Truck className="h-6 w-6 text-orange-400" />
<div>
<h3 className="text-lg font-semibold text-white">Логистика</h3>
<p className="text-white/60 text-sm">Найдите и добавьте логистические компании в контрагенты</p>
</div>
</div>
{/* Результаты поиска */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Поиск...</div>
</div>
) : organizations.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<Truck className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{searchTerm ? 'Логистические компании не найдены' : 'Введите запрос для поиска'}
</p>
<p className="text-white/40 text-sm mt-2">
Попробуйте изменить условия поиска
</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{organizations.map((organization: Organization) => (
<OrganizationCard
key={organization.id}
organization={organization}
onSendRequest={handleSendRequest}
actionButtonText="Добавить"
actionButtonColor="yellow"
requestSending={sendingRequest}
/>
))}
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,125 @@
"use client"
import { useState } from 'react'
import { useQuery, useMutation } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Search, ShoppingCart } from 'lucide-react'
import { OrganizationCard } from './organization-card'
import { SEARCH_ORGANIZATIONS } from '@/graphql/queries'
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
createdAt: string
users?: Array<{ id: string, avatar?: string }>
isCounterparty?: boolean
}
export function MarketSellers() {
const [searchTerm, setSearchTerm] = useState('')
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
variables: { type: 'SELLER', search: searchTerm || null }
})
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
onCompleted: () => {
refetch()
}
})
const handleSearch = () => {
refetch({ type: 'SELLER', search: searchTerm || null })
}
const handleSendRequest = async (organizationId: string, message: string) => {
try {
await sendRequest({
variables: {
receiverId: organizationId,
message: message || 'Заявка на добавление в контрагенты'
}
})
} catch (error) {
console.error('Ошибка отправки заявки:', error)
}
}
const organizations = data?.searchOrganizations || []
return (
<div className="h-full flex flex-col space-y-4 overflow-hidden">
{/* Поиск */}
<div className="flex space-x-4 flex-shrink-0">
<div className="relative flex-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск селлеров по названию или ИНН..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<Button
onClick={handleSearch}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30 cursor-pointer"
>
<Search className="h-4 w-4 mr-2" />
Найти
</Button>
</div>
{/* Заголовок с иконкой */}
<div className="flex items-center space-x-3 flex-shrink-0 mb-4">
<ShoppingCart className="h-6 w-6 text-green-400" />
<div>
<h3 className="text-lg font-semibold text-white">Селлеры</h3>
<p className="text-white/60 text-sm">Найдите и добавьте селлеров в контрагенты</p>
</div>
</div>
{/* Результаты поиска */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Поиск...</div>
</div>
) : organizations.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<ShoppingCart className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{searchTerm ? 'Селлеры не найдены' : 'Введите запрос для поиска селлеров'}
</p>
<p className="text-white/40 text-sm mt-2">
Попробуйте изменить условия поиска
</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{organizations.map((organization: Organization) => (
<OrganizationCard
key={organization.id}
organization={organization}
onSendRequest={handleSendRequest}
actionButtonText="Добавить"
actionButtonColor="orange"
requestSending={sendingRequest}
/>
))}
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,125 @@
"use client"
import { useState } from 'react'
import { useQuery, useMutation } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Search, Boxes } from 'lucide-react'
import { OrganizationCard } from './organization-card'
import { SEARCH_ORGANIZATIONS } from '@/graphql/queries'
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
createdAt: string
users?: Array<{ id: string, avatar?: string }>
isCounterparty?: boolean
}
export function MarketWholesale() {
const [searchTerm, setSearchTerm] = useState('')
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
variables: { type: 'WHOLESALE', search: searchTerm || null }
})
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
onCompleted: () => {
refetch()
}
})
const handleSearch = () => {
refetch({ type: 'WHOLESALE', search: searchTerm || null })
}
const handleSendRequest = async (organizationId: string, message: string) => {
try {
await sendRequest({
variables: {
receiverId: organizationId,
message: message || 'Заявка на добавление в контрагенты'
}
})
} catch (error) {
console.error('Ошибка отправки заявки:', error)
}
}
const organizations = data?.searchOrganizations || []
return (
<div className="h-full flex flex-col space-y-4 overflow-hidden">
{/* Поиск */}
<div className="flex space-x-4 flex-shrink-0">
<div className="relative flex-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск оптовых компаний по названию или ИНН..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<Button
onClick={handleSearch}
className="bg-purple-500/20 hover:bg-purple-500/30 text-purple-300 border-purple-500/30 cursor-pointer"
>
<Search className="h-4 w-4 mr-2" />
Найти
</Button>
</div>
{/* Заголовок с иконкой */}
<div className="flex items-center space-x-3 flex-shrink-0 mb-4">
<Boxes className="h-6 w-6 text-purple-400" />
<div>
<h3 className="text-lg font-semibold text-white">Оптовики</h3>
<p className="text-white/60 text-sm">Найдите и добавьте оптовые компании в контрагенты</p>
</div>
</div>
{/* Результаты поиска */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Поиск...</div>
</div>
) : organizations.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<Boxes className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{searchTerm ? 'Оптовые компании не найдены' : 'Введите запрос для поиска оптовиков'}
</p>
<p className="text-white/40 text-sm mt-2">
Попробуйте изменить условия поиска
</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{organizations.map((organization: Organization) => (
<OrganizationCard
key={organization.id}
organization={organization}
onSendRequest={handleSendRequest}
actionButtonText="Добавить"
actionButtonColor="red"
requestSending={sendingRequest}
/>
))}
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,92 @@
"use client"
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { cn } from '@/lib/utils'
interface User {
id: string
avatar?: string | null
}
interface Organization {
id: string
name?: string | null
fullName?: string | null
users?: User[]
}
interface OrganizationAvatarProps {
organization: Organization
size?: 'sm' | 'md' | 'lg'
className?: string
}
// Цвета для fallback аватарок
const FALLBACK_COLORS = [
'bg-blue-500',
'bg-green-500',
'bg-purple-500',
'bg-orange-500',
'bg-pink-500',
'bg-indigo-500',
'bg-teal-500',
'bg-red-500',
'bg-yellow-500',
'bg-cyan-500'
]
function getInitials(name: string): string {
return name
.split(' ')
.map(word => word.charAt(0))
.join('')
.toUpperCase()
.slice(0, 2)
}
function getColorForOrganization(organizationId: string): string {
const hash = organizationId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return FALLBACK_COLORS[hash % FALLBACK_COLORS.length]
}
function getSizes(size: 'sm' | 'md' | 'lg') {
switch (size) {
case 'sm':
return { avatar: 'size-8', text: 'text-xs' }
case 'md':
return { avatar: 'size-10', text: 'text-sm' }
case 'lg':
return { avatar: 'size-12', text: 'text-base' }
default:
return { avatar: 'size-8', text: 'text-xs' }
}
}
export function OrganizationAvatar({
organization,
size = 'md',
className
}: OrganizationAvatarProps) {
// Берем аватарку первого пользователя организации
const userAvatar = organization.users?.[0]?.avatar
// Получаем имя для инициалов
const displayName = organization.name || organization.fullName || 'Организация'
const initials = getInitials(displayName)
// Получаем цвет для fallback
const fallbackColor = getColorForOrganization(organization.id)
const sizes = getSizes(size)
return (
<Avatar className={cn(sizes.avatar, className)}>
{userAvatar && (
<AvatarImage src={userAvatar} alt={displayName} />
)}
<AvatarFallback className={cn(fallbackColor, 'text-white font-medium', sizes.text)}>
{initials}
</AvatarFallback>
</Avatar>
)
}

View File

@ -0,0 +1,268 @@
"use client"
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import {
Phone,
Mail,
MapPin,
Calendar,
Plus,
Send,
Trash2
} from 'lucide-react'
import { OrganizationAvatar } from './organization-avatar'
import { useState } from 'react'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
createdAt: string
users?: Array<{ id: string, avatar?: string }>
isCounterparty?: boolean
}
interface OrganizationCardProps {
organization: Organization
onSendRequest?: (organizationId: string, message: string) => void
onRemove?: (organizationId: string) => void
showRemoveButton?: boolean
actionButtonText?: string
actionButtonColor?: string
requestSending?: boolean
}
export function OrganizationCard({
organization,
onSendRequest,
onRemove,
showRemoveButton = false,
actionButtonText = "Добавить",
actionButtonColor = "green",
requestSending = false
}: OrganizationCardProps) {
const [requestMessage, setRequestMessage] = useState('')
const [isDialogOpen, setIsDialogOpen] = useState(false)
const formatDate = (dateString: string) => {
if (!dateString) return ''
try {
let date: Date
// Проверяем, является ли строка числом (Unix timestamp)
if (/^\d+$/.test(dateString)) {
// Если это Unix timestamp в миллисекундах
const timestamp = parseInt(dateString, 10)
date = new Date(timestamp)
} else {
// Обычная строка даты
date = new Date(dateString)
}
if (isNaN(date.getTime())) {
return 'Неверная дата'
}
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
} catch (error) {
return 'Ошибка даты'
}
}
const getTypeLabel = (type: string) => {
switch (type) {
case 'FULFILLMENT': return 'Фулфилмент'
case 'SELLER': return 'Селлер'
case 'LOGIST': return 'Логистика'
case 'WHOLESALE': return 'Оптовик'
default: return type
}
}
const getTypeColor = (type: string) => {
switch (type) {
case 'FULFILLMENT': return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
case 'SELLER': return 'bg-green-500/20 text-green-300 border-green-500/30'
case 'LOGIST': return 'bg-orange-500/20 text-orange-300 border-orange-500/30'
case 'WHOLESALE': return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
default: return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
}
}
const getActionButtonColor = (color: string, isDisabled: boolean) => {
if (isDisabled) {
return "bg-gray-500/20 text-gray-400 border-gray-500/30 cursor-not-allowed"
}
switch (color) {
case 'green': return 'bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30'
case 'orange': return 'bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border-orange-500/30'
case 'yellow': return 'bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border-yellow-500/30'
case 'red': return 'bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30'
case 'blue': return 'bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30'
default: return 'bg-gray-500/20 hover:bg-gray-500/30 text-gray-300 border-gray-500/30'
}
}
const handleSendRequest = () => {
if (onSendRequest) {
onSendRequest(organization.id, requestMessage)
setRequestMessage('')
setIsDialogOpen(false)
}
}
const handleRemove = () => {
if (onRemove) {
onRemove(organization.id)
}
}
return (
<div className="glass-card p-4 w-full">
<div className="flex flex-col space-y-4">
<div className="flex items-start space-x-3">
<OrganizationAvatar organization={organization} size="md" />
<div className="flex-1 min-w-0">
<div className="flex flex-col space-y-2 mb-3">
<h4 className="text-white font-medium text-lg leading-tight">
{organization.name || organization.fullName}
</h4>
<div className="flex items-center space-x-3">
<Badge className={getTypeColor(organization.type)}>
{getTypeLabel(organization.type)}
</Badge>
{organization.isCounterparty && (
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">
Уже добавлен
</Badge>
)}
</div>
</div>
<div className="space-y-2">
<p className="text-white/60 text-sm">ИНН: {organization.inn}</p>
{organization.address && (
<div className="flex items-center text-white/60 text-sm">
<MapPin className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="truncate">{organization.address}</span>
</div>
)}
{organization.phones && organization.phones.length > 0 && (
<div className="flex items-center text-white/60 text-sm">
<Phone className="h-4 w-4 mr-2 flex-shrink-0" />
<span>{organization.phones[0].value}</span>
</div>
)}
{organization.emails && organization.emails.length > 0 && (
<div className="flex items-center text-white/60 text-sm">
<Mail className="h-4 w-4 mr-2 flex-shrink-0" />
<span className="truncate">{organization.emails[0].value}</span>
</div>
)}
<div className="flex items-center text-white/40 text-xs">
<Calendar className="h-4 w-4 mr-2 flex-shrink-0" />
<span>{showRemoveButton ? 'Добавлен' : 'Зарегистрирован'} {formatDate(organization.createdAt)}</span>
</div>
</div>
</div>
</div>
{showRemoveButton ? (
<Button
size="sm"
variant="outline"
onClick={handleRemove}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer w-full"
>
<Trash2 className="h-4 w-4 mr-2" />
Удалить из контрагентов
</Button>
) : (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
size="sm"
disabled={organization.isCounterparty}
className={`${getActionButtonColor(actionButtonColor, !!organization.isCounterparty)} w-full cursor-pointer`}
>
<Plus className="h-4 w-4 mr-2" />
{organization.isCounterparty ? 'Уже добавлен' : actionButtonText}
</Button>
</DialogTrigger>
<DialogContent className="bg-gray-900/95 backdrop-blur border-white/10 text-white">
<DialogHeader>
<DialogTitle className="text-white">
Отправить заявку в контрагенты
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-white/5 rounded-lg border border-white/10">
<div className="flex items-center space-x-3">
<OrganizationAvatar organization={organization} size="sm" />
<div>
<h4 className="text-white font-medium">
{organization.name || organization.fullName}
</h4>
<p className="text-white/60 text-sm">ИНН: {organization.inn}</p>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
Сообщение (необязательно)
</label>
<Input
placeholder="Добавьте комментарий к заявке..."
value={requestMessage}
onChange={(e) => setRequestMessage(e.target.value)}
className="glass-input text-white placeholder:text-white/40"
/>
</div>
<div className="flex space-x-3 pt-4">
<Button
onClick={() => setIsDialogOpen(false)}
variant="outline"
className="flex-1 bg-white/5 hover:bg-white/10 text-white border-white/20 cursor-pointer"
>
Отмена
</Button>
<Button
onClick={handleSendRequest}
disabled={requestSending}
className="flex-1 bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30 cursor-pointer"
>
{requestSending ? (
"Отправка..."
) : (
<>
<Send className="h-4 w-4 mr-2" />
Отправить
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,431 @@
"use client"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import {
Building2,
Phone,
Mail,
MapPin,
Calendar,
FileText,
Users,
CreditCard,
Hash,
User,
Briefcase
} from 'lucide-react'
import { OrganizationAvatar } from './organization-avatar'
interface User {
id: string
avatar?: string | null
phone: string
createdAt: string
}
interface ApiKey {
id: string
marketplace: string
isActive: boolean
createdAt: string
}
interface Organization {
id: string
inn: string
kpp?: string | null
name?: string | null
fullName?: string | null
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string | null
addressFull?: string | null
ogrn?: string | null
ogrnDate?: string | null
status?: string | null
actualityDate?: string | null
registrationDate?: string | null
liquidationDate?: string | null
managementName?: string | null
managementPost?: string | null
opfCode?: string | null
opfFull?: string | null
opfShort?: string | null
okato?: string | null
oktmo?: string | null
okpo?: string | null
okved?: string | null
employeeCount?: number | null
revenue?: string | null
taxSystem?: string | null
phones?: Array<{ value: string }> | null
emails?: Array<{ value: string }> | null
users?: User[]
apiKeys?: ApiKey[]
createdAt: string
}
interface OrganizationDetailsModalProps {
organization: Organization | null
open: boolean
onOpenChange: (open: boolean) => void
}
function formatDate(dateString?: string | null): string {
if (!dateString) return 'Не указана'
try {
let date: Date
// Проверяем, является ли строка числом (Unix timestamp)
if (/^\d+$/.test(dateString)) {
// Если это Unix timestamp в миллисекундах
const timestamp = parseInt(dateString, 10)
date = new Date(timestamp)
} else {
// Обычная строка даты
date = new Date(dateString)
}
if (isNaN(date.getTime())) {
return 'Не указана'
}
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
} catch (error) {
return 'Не указана'
}
}
function getTypeLabel(type: string): string {
switch (type) {
case 'FULFILLMENT':
return 'Фулфилмент'
case 'SELLER':
return 'Селлер'
case 'LOGIST':
return 'Логистика'
case 'WHOLESALE':
return 'Оптовик'
default:
return type
}
}
function getTypeColor(type: string): string {
switch (type) {
case 'FULFILLMENT':
return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
case 'SELLER':
return 'bg-green-500/20 text-green-300 border-green-500/30'
case 'LOGIST':
return 'bg-orange-500/20 text-orange-300 border-orange-500/30'
case 'WHOLESALE':
return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
default:
return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
}
}
export function OrganizationDetailsModal({ organization, open, onOpenChange }: OrganizationDetailsModalProps) {
if (!organization) return null
const displayName = organization.name || organization.fullName || 'Неизвестная организация'
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto bg-black/90 backdrop-blur-xl border border-white/20">
<DialogHeader>
<DialogTitle className="flex items-center space-x-4 text-white">
<OrganizationAvatar organization={organization} size="lg" />
<div>
<h2 className="text-xl font-semibold">{displayName}</h2>
<Badge className={getTypeColor(organization.type)}>
{getTypeLabel(organization.type)}
</Badge>
</div>
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Основная информация */}
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<Building2 className="h-5 w-5 mr-2 text-blue-400" />
Основная информация
</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-white/60">ИНН:</span>
<span className="text-white font-mono">{organization.inn}</span>
</div>
{organization.kpp && (
<div className="flex justify-between">
<span className="text-white/60">КПП:</span>
<span className="text-white font-mono">{organization.kpp}</span>
</div>
)}
{organization.ogrn && (
<div className="flex justify-between">
<span className="text-white/60">ОГРН:</span>
<span className="text-white font-mono">{organization.ogrn}</span>
</div>
)}
{organization.status && (
<div className="flex justify-between">
<span className="text-white/60">Статус:</span>
<span className="text-white">{organization.status}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-white/60">Дата регистрации:</span>
<span className="text-white">{formatDate(organization.registrationDate)}</span>
</div>
</div>
</Card>
{/* Контактная информация */}
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<Phone className="h-5 w-5 mr-2 text-green-400" />
Контакты
</h3>
<div className="space-y-3">
{organization.phones && organization.phones.length > 0 && (
<div>
<div className="text-white/60 text-sm mb-2">Телефоны:</div>
{organization.phones.map((phone, index) => (
<div key={index} className="flex items-center text-white">
<Phone className="h-3 w-3 mr-2 text-green-400" />
{phone.value}
</div>
))}
</div>
)}
{organization.emails && organization.emails.length > 0 && (
<div>
<div className="text-white/60 text-sm mb-2">Email:</div>
{organization.emails.map((email, index) => (
<div key={index} className="flex items-center text-white">
<Mail className="h-3 w-3 mr-2 text-blue-400" />
{email.value}
</div>
))}
</div>
)}
{organization.address && (
<div>
<div className="text-white/60 text-sm mb-2">Адрес:</div>
<div className="flex items-start text-white">
<MapPin className="h-3 w-3 mr-2 mt-1 text-orange-400 flex-shrink-0" />
<span className="text-sm">{organization.addressFull || organization.address}</span>
</div>
</div>
)}
</div>
</Card>
{/* Руководство */}
{organization.managementName && (
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<User className="h-5 w-5 mr-2 text-purple-400" />
Руководство
</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-white/60">Руководитель:</span>
<span className="text-white">{organization.managementName}</span>
</div>
{organization.managementPost && (
<div className="flex justify-between">
<span className="text-white/60">Должность:</span>
<span className="text-white">{organization.managementPost}</span>
</div>
)}
</div>
</Card>
)}
{/* Организационно-правовая форма */}
{organization.opfFull && (
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<FileText className="h-5 w-5 mr-2 text-yellow-400" />
ОПФ
</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-white/60">Полное название:</span>
<span className="text-white">{organization.opfFull}</span>
</div>
{organization.opfShort && (
<div className="flex justify-between">
<span className="text-white/60">Краткое название:</span>
<span className="text-white">{organization.opfShort}</span>
</div>
)}
{organization.opfCode && (
<div className="flex justify-between">
<span className="text-white/60">Код ОКОПФ:</span>
<span className="text-white font-mono">{organization.opfCode}</span>
</div>
)}
</div>
</Card>
)}
{/* Коды статистики */}
{(organization.okato || organization.oktmo || organization.okpo || organization.okved) && (
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<Hash className="h-5 w-5 mr-2 text-cyan-400" />
Коды статистики
</h3>
<div className="space-y-3">
{organization.okato && (
<div className="flex justify-between">
<span className="text-white/60">ОКАТО:</span>
<span className="text-white font-mono">{organization.okato}</span>
</div>
)}
{organization.oktmo && (
<div className="flex justify-between">
<span className="text-white/60">ОКТМО:</span>
<span className="text-white font-mono">{organization.oktmo}</span>
</div>
)}
{organization.okpo && (
<div className="flex justify-between">
<span className="text-white/60">ОКПО:</span>
<span className="text-white font-mono">{organization.okpo}</span>
</div>
)}
{organization.okved && (
<div className="flex justify-between">
<span className="text-white/60">Основной ОКВЭД:</span>
<span className="text-white font-mono">{organization.okved}</span>
</div>
)}
</div>
</Card>
)}
{/* Финансовая информация */}
{(organization.employeeCount || organization.revenue || organization.taxSystem) && (
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<CreditCard className="h-5 w-5 mr-2 text-emerald-400" />
Финансовая информация
</h3>
<div className="space-y-3">
{organization.employeeCount && (
<div className="flex justify-between">
<span className="text-white/60">Сотрудников:</span>
<span className="text-white">{organization.employeeCount}</span>
</div>
)}
{organization.revenue && (
<div className="flex justify-between">
<span className="text-white/60">Выручка:</span>
<span className="text-white">{organization.revenue}</span>
</div>
)}
{organization.taxSystem && (
<div className="flex justify-between">
<span className="text-white/60">Налоговая система:</span>
<span className="text-white">{organization.taxSystem}</span>
</div>
)}
</div>
</Card>
)}
{/* Пользователи */}
{organization.users && organization.users.length > 0 && (
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<Users className="h-5 w-5 mr-2 text-indigo-400" />
Пользователи ({organization.users.length})
</h3>
<div className="space-y-3">
{organization.users.map((user, index) => (
<div key={user.id} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<OrganizationAvatar
organization={{
id: user.id,
users: [user]
}}
size="sm"
/>
<span className="text-white">{user.phone}</span>
</div>
<span className="text-white/60 text-sm">
{formatDate(user.createdAt)}
</span>
</div>
))}
</div>
</Card>
)}
{/* API ключи */}
{organization.apiKeys && organization.apiKeys.length > 0 && (
<Card className="glass-card p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<Briefcase className="h-5 w-5 mr-2 text-pink-400" />
API ключи маркетплейсов
</h3>
<div className="space-y-3">
{organization.apiKeys.map((apiKey, index) => (
<div key={apiKey.id} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Badge className={apiKey.isActive ? 'bg-green-500/20 text-green-300 border-green-500/30' : 'bg-red-500/20 text-red-300 border-red-500/30'}>
{apiKey.marketplace}
</Badge>
<span className="text-white/60 text-sm">
{apiKey.isActive ? 'Активен' : 'Неактивен'}
</span>
</div>
<span className="text-white/60 text-sm">
{formatDate(apiKey.createdAt)}
</span>
</div>
))}
</div>
</Card>
)}
</div>
</DialogContent>
</Dialog>
)
}