Добавлены функции фильтрации и сортировки контрагентов в компонент MarketCounterparties. Реализованы поля для поиска, выбора типа и порядка сортировки. Обновлены стили и структура кода для улучшения взаимодействия с пользователем. Оптимизированы запросы к API для получения данных о контрагентах.
This commit is contained in:
@ -1,15 +1,27 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React from 'react'
|
import React, { useState, useMemo } from 'react'
|
||||||
import { useQuery, useMutation } from '@apollo/client'
|
import { useQuery, useMutation } from '@apollo/client'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { GlassInput } from '@/components/ui/input'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
ArrowUpCircle,
|
ArrowUpCircle,
|
||||||
ArrowDownCircle
|
ArrowDownCircle,
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
|
SortAsc,
|
||||||
|
SortDesc,
|
||||||
|
Calendar,
|
||||||
|
Building,
|
||||||
|
Phone,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
X
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { OrganizationCard } from './organization-card'
|
import { OrganizationCard } from './organization-card'
|
||||||
import { OrganizationAvatar } from './organization-avatar'
|
import { OrganizationAvatar } from './organization-avatar'
|
||||||
@ -38,7 +50,15 @@ interface CounterpartyRequest {
|
|||||||
receiver: Organization
|
receiver: Organization
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SortField = 'name' | 'date' | 'inn' | 'type'
|
||||||
|
type SortOrder = 'asc' | 'desc'
|
||||||
|
|
||||||
export function MarketCounterparties() {
|
export function MarketCounterparties() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>('all')
|
||||||
|
const [sortField, setSortField] = useState<SortField>('name')
|
||||||
|
const [sortOrder, setSortOrder] = useState<SortOrder>('asc')
|
||||||
|
|
||||||
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
|
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
|
||||||
const { data: incomingData, loading: incomingLoading } = useQuery(GET_INCOMING_REQUESTS)
|
const { data: incomingData, loading: incomingLoading } = useQuery(GET_INCOMING_REQUESTS)
|
||||||
const { data: outgoingData, loading: outgoingLoading } = useQuery(GET_OUTGOING_REQUESTS)
|
const { data: outgoingData, loading: outgoingLoading } = useQuery(GET_OUTGOING_REQUESTS)
|
||||||
@ -77,6 +97,56 @@ export function MarketCounterparties() {
|
|||||||
awaitRefetchQueries: true
|
awaitRefetchQueries: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Фильтрация и сортировка контрагентов
|
||||||
|
const filteredAndSortedCounterparties = useMemo(() => {
|
||||||
|
const filtered = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => {
|
||||||
|
const matchesSearch = !searchQuery ||
|
||||||
|
(org.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
org.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
org.inn.includes(searchQuery) ||
|
||||||
|
org.address?.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
|
||||||
|
const matchesType = typeFilter === 'all' || org.type === typeFilter
|
||||||
|
|
||||||
|
return matchesSearch && matchesType
|
||||||
|
})
|
||||||
|
|
||||||
|
// Сортировка
|
||||||
|
filtered.sort((a: Organization, b: Organization) => {
|
||||||
|
let aValue: string | number
|
||||||
|
let bValue: string | number
|
||||||
|
|
||||||
|
switch (sortField) {
|
||||||
|
case 'name':
|
||||||
|
aValue = (a.name || a.fullName || '').toLowerCase()
|
||||||
|
bValue = (b.name || b.fullName || '').toLowerCase()
|
||||||
|
break
|
||||||
|
case 'date':
|
||||||
|
aValue = new Date(a.createdAt).getTime()
|
||||||
|
bValue = new Date(b.createdAt).getTime()
|
||||||
|
break
|
||||||
|
case 'inn':
|
||||||
|
aValue = a.inn
|
||||||
|
bValue = b.inn
|
||||||
|
break
|
||||||
|
case 'type':
|
||||||
|
aValue = a.type
|
||||||
|
bValue = b.type
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrder === 'asc') {
|
||||||
|
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0
|
||||||
|
} else {
|
||||||
|
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}, [counterpartiesData?.myCounterparties, searchQuery, typeFilter, sortField, sortOrder])
|
||||||
|
|
||||||
const handleAcceptRequest = async (requestId: string) => {
|
const handleAcceptRequest = async (requestId: string) => {
|
||||||
try {
|
try {
|
||||||
await respondToRequest({
|
await respondToRequest({
|
||||||
@ -146,20 +216,41 @@ export function MarketCounterparties() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setSearchQuery('')
|
||||||
|
setTypeFilter('all')
|
||||||
|
setSortField('name')
|
||||||
|
setSortOrder('asc')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActiveFilters = searchQuery || typeFilter !== 'all' || sortField !== 'name' || sortOrder !== 'asc'
|
||||||
|
|
||||||
const counterparties = counterpartiesData?.myCounterparties || []
|
const counterparties = counterpartiesData?.myCounterparties || []
|
||||||
const incomingRequests = incomingData?.incomingRequests || []
|
const incomingRequests = incomingData?.incomingRequests || []
|
||||||
const outgoingRequests = outgoingData?.outgoingRequests || []
|
const outgoingRequests = outgoingData?.outgoingRequests || []
|
||||||
|
|
||||||
|
const getTypeLabel = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'FULFILLMENT': return 'Фулфилмент'
|
||||||
|
case 'SELLER': return 'Селлер'
|
||||||
|
case 'LOGIST': return 'Логистика'
|
||||||
|
case 'WHOLESALE': return 'Оптовик'
|
||||||
|
default: return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeBadgeStyles = (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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<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">
|
<div className="flex-1 overflow-hidden">
|
||||||
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
|
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
|
||||||
<TabsList className="grid w-full grid-cols-3 bg-white/5 border-white/10">
|
<TabsList className="grid w-full grid-cols-3 bg-white/5 border-white/10">
|
||||||
@ -180,33 +271,200 @@ export function MarketCounterparties() {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="counterparties" className="flex-1 overflow-auto mt-4">
|
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-3 flex flex-col">
|
||||||
|
{/* Компактная панель фильтров */}
|
||||||
|
<div className="glass-card p-3 mb-3 space-y-3">
|
||||||
|
<div className="flex flex-col xl:flex-row gap-3">
|
||||||
|
{/* Поиск */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
|
||||||
|
<GlassInput
|
||||||
|
placeholder="Поиск..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Фильтры и сортировка */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||||
|
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[120px]">
|
||||||
|
<Filter className="h-3 w-3 mr-1" />
|
||||||
|
<SelectValue placeholder="Тип" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="glass-card border-white/20">
|
||||||
|
<SelectItem value="all">Все</SelectItem>
|
||||||
|
<SelectItem value="FULFILLMENT">Фулфилмент</SelectItem>
|
||||||
|
<SelectItem value="SELLER">Селлер</SelectItem>
|
||||||
|
<SelectItem value="LOGIST">Логистика</SelectItem>
|
||||||
|
<SelectItem value="WHOLESALE">Оптовик</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={sortField} onValueChange={(value) => setSortField(value as SortField)}>
|
||||||
|
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[100px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="glass-card border-white/20">
|
||||||
|
<SelectItem value="name">Название</SelectItem>
|
||||||
|
<SelectItem value="date">Дата</SelectItem>
|
||||||
|
<SelectItem value="inn">ИНН</SelectItem>
|
||||||
|
<SelectItem value="type">Тип</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||||
|
className="glass-input border-white/20 text-white hover:bg-white/10 h-9 w-9 p-0"
|
||||||
|
>
|
||||||
|
{sortOrder === 'asc' ? <SortAsc className="h-3 w-3" /> : <SortDesc className="h-3 w-3" />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearFilters}
|
||||||
|
className="text-white/60 hover:text-white hover:bg-white/10 h-9 w-9 p-0"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Статистика и быстрые фильтры */}
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<div className="text-white/60">
|
||||||
|
{filteredAndSortedCounterparties.length} из {counterparties.length}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{['FULFILLMENT', 'SELLER', 'LOGIST', 'WHOLESALE'].map((type) => {
|
||||||
|
const count = counterparties.filter((org: Organization) => org.type === type).length
|
||||||
|
if (count === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={type}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTypeFilter(typeFilter === type ? 'all' : type)}
|
||||||
|
className={`h-6 px-2 text-xs ${
|
||||||
|
typeFilter === type
|
||||||
|
? getTypeBadgeStyles(type) + ' border'
|
||||||
|
: 'text-white/50 hover:text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getTypeLabel(type)} ({count})
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Список контрагентов */}
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
{counterpartiesLoading ? (
|
{counterpartiesLoading ? (
|
||||||
<div className="flex items-center justify-center p-8">
|
<div className="flex items-center justify-center p-8">
|
||||||
<div className="text-white/60">Загрузка...</div>
|
<div className="text-white/60">Загрузка...</div>
|
||||||
</div>
|
</div>
|
||||||
) : counterparties.length === 0 ? (
|
) : filteredAndSortedCounterparties.length === 0 ? (
|
||||||
<div className="glass-card p-8">
|
<div className="glass-card p-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
{counterparties.length === 0 ? (
|
||||||
|
<>
|
||||||
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||||
<p className="text-white/60">У вас пока нет контрагентов</p>
|
<p className="text-white/60">У вас пока нет контрагентов</p>
|
||||||
<p className="text-white/40 text-sm mt-2">
|
<p className="text-white/40 text-sm mt-2">
|
||||||
Перейдите на другие вкладки, чтобы найти партнеров
|
Перейдите на другие вкладки, чтобы найти партнеров
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Search 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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{counterparties.map((organization: Organization) => (
|
{filteredAndSortedCounterparties.map((organization: Organization) => (
|
||||||
<OrganizationCard
|
<div key={organization.id} className="glass-card p-4 w-full hover:bg-white/5 transition-colors">
|
||||||
key={organization.id}
|
<div className="flex flex-col space-y-4">
|
||||||
organization={organization}
|
<div className="flex items-start space-x-3">
|
||||||
onRemove={handleRemoveCounterparty}
|
<OrganizationAvatar organization={organization} size="md" />
|
||||||
showRemoveButton={true}
|
<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-2">
|
||||||
|
<Badge className={getTypeBadgeStyles(organization.type)}>
|
||||||
|
{getTypeLabel(organization.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center text-white/60 text-sm">
|
||||||
|
<Building className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||||
|
<span>ИНН: {organization.inn}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{organization.address && (
|
||||||
|
<div className="flex items-start text-white/60 text-sm">
|
||||||
|
<MapPin className="h-3 w-3 mr-2 mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="line-clamp-2">{organization.address}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{organization.phones && organization.phones.length > 0 && (
|
||||||
|
<div className="flex items-center text-white/60 text-sm">
|
||||||
|
<Phone className="h-3 w-3 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-3 w-3 mr-2 flex-shrink-0" />
|
||||||
|
<span className="truncate">{organization.emails[0].value}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center text-white/40 text-xs pt-2">
|
||||||
|
<Calendar className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||||
|
<span>Добавлен {formatDate(organization.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleRemoveCounterparty(organization.id)}
|
||||||
|
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer w-full"
|
||||||
|
>
|
||||||
|
Удалить из контрагентов
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="incoming" className="flex-1 overflow-auto mt-4">
|
<TabsContent value="incoming" className="flex-1 overflow-auto mt-4">
|
||||||
@ -234,18 +492,8 @@ export function MarketCounterparties() {
|
|||||||
{request.sender.name || request.sender.fullName}
|
{request.sender.name || request.sender.fullName}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Badge className={
|
<Badge className={getTypeBadgeStyles(request.sender.type)}>
|
||||||
request.sender.type === 'FULFILLMENT' ? 'bg-blue-500/20 text-blue-300 border-blue-500/30' :
|
{getTypeLabel(request.sender.type)}
|
||||||
request.sender.type === 'SELLER' ? 'bg-green-500/20 text-green-300 border-green-500/30' :
|
|
||||||
request.sender.type === 'LOGIST' ? 'bg-orange-500/20 text-orange-300 border-orange-500/30' :
|
|
||||||
request.sender.type === 'WHOLESALE' ? 'bg-purple-500/20 text-purple-300 border-purple-500/30' :
|
|
||||||
'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
|
||||||
}>
|
|
||||||
{request.sender.type === 'FULFILLMENT' ? 'Фулфилмент' :
|
|
||||||
request.sender.type === 'SELLER' ? 'Селлер' :
|
|
||||||
request.sender.type === 'LOGIST' ? 'Логистика' :
|
|
||||||
request.sender.type === 'WHOLESALE' ? 'Оптовик' :
|
|
||||||
request.sender.type}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -318,18 +566,8 @@ export function MarketCounterparties() {
|
|||||||
{request.receiver.name || request.receiver.fullName}
|
{request.receiver.name || request.receiver.fullName}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Badge className={
|
<Badge className={getTypeBadgeStyles(request.receiver.type)}>
|
||||||
request.receiver.type === 'FULFILLMENT' ? 'bg-blue-500/20 text-blue-300 border-blue-500/30' :
|
{getTypeLabel(request.receiver.type)}
|
||||||
request.receiver.type === 'SELLER' ? 'bg-green-500/20 text-green-300 border-green-500/30' :
|
|
||||||
request.receiver.type === 'LOGIST' ? 'bg-orange-500/20 text-orange-300 border-orange-500/30' :
|
|
||||||
request.receiver.type === 'WHOLESALE' ? 'bg-purple-500/20 text-purple-300 border-purple-500/30' :
|
|
||||||
'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
|
||||||
}>
|
|
||||||
{request.receiver.type === 'FULFILLMENT' ? 'Фулфилмент' :
|
|
||||||
request.receiver.type === 'SELLER' ? 'Селлер' :
|
|
||||||
request.receiver.type === 'LOGIST' ? 'Логистика' :
|
|
||||||
request.receiver.type === 'WHOLESALE' ? 'Оптовик' :
|
|
||||||
request.receiver.type}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge className={
|
<Badge className={
|
||||||
request.status === 'PENDING' ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' :
|
request.status === 'PENDING' ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' :
|
||||||
|
@ -437,7 +437,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
card.object.toLowerCase().includes(searchTerm.toLowerCase())
|
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
setWbCards(filteredCards)
|
setWbCards(filteredCards)
|
||||||
@ -450,7 +450,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
card.object.toLowerCase().includes(searchTerm.toLowerCase())
|
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
setWbCards(filteredCards)
|
setWbCards(filteredCards)
|
||||||
console.log('Найдено моковых товаров (fallback):', filteredCards.length)
|
console.log('Найдено моковых товаров (fallback):', filteredCards.length)
|
||||||
@ -573,14 +573,20 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextImage = () => {
|
const nextImage = () => {
|
||||||
if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) {
|
if (selectedCardForDetails) {
|
||||||
setCurrentImageIndex((prev) => (prev + 1) % selectedCardForDetails.mediaFiles.length)
|
const images = WildberriesService.getCardImages(selectedCardForDetails)
|
||||||
|
if (images.length > 1) {
|
||||||
|
setCurrentImageIndex((prev) => (prev + 1) % images.length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevImage = () => {
|
const prevImage = () => {
|
||||||
if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) {
|
if (selectedCardForDetails) {
|
||||||
setCurrentImageIndex((prev) => (prev - 1 + selectedCardForDetails.mediaFiles.length) % selectedCardForDetails.mediaFiles.length)
|
const images = WildberriesService.getCardImages(selectedCardForDetails)
|
||||||
|
if (images.length > 1) {
|
||||||
|
setCurrentImageIndex((prev) => (prev - 1 + images.length) % images.length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,7 +658,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
<Card key={sc.card.nmID} className="bg-white/10 backdrop-blur border-white/20 p-4">
|
<Card key={sc.card.nmID} className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<img
|
<img
|
||||||
src={sc.card.mediaFiles?.[0] || '/api/placeholder/120/120'}
|
src={WildberriesService.getCardImage(sc.card, 'c246x328') || '/api/placeholder/120/120'}
|
||||||
alt={sc.card.title}
|
alt={sc.card.title}
|
||||||
className="w-20 h-20 rounded-lg object-cover"
|
className="w-20 h-20 rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
@ -1017,7 +1023,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="aspect-square relative bg-white/5 overflow-hidden rounded-lg">
|
<div className="aspect-square relative bg-white/5 overflow-hidden rounded-lg">
|
||||||
<img
|
<img
|
||||||
src={card.mediaFiles?.[0] || '/api/placeholder/300/300'}
|
src={WildberriesService.getCardImage(card, 'c516x688') || '/api/placeholder/300/300'}
|
||||||
alt={card.title}
|
alt={card.title}
|
||||||
className="w-full h-full object-cover cursor-pointer group-hover:scale-110 transition-transform duration-500"
|
className="w-full h-full object-cover cursor-pointer group-hover:scale-110 transition-transform duration-500"
|
||||||
onClick={() => handleCardClick(card)}
|
onClick={() => handleCardClick(card)}
|
||||||
@ -1201,13 +1207,13 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
<div className="p-4 lg:p-6 space-y-4 overflow-hidden">
|
<div className="p-4 lg:p-6 space-y-4 overflow-hidden">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<img
|
||||||
src={selectedCardForDetails.mediaFiles?.[currentImageIndex] || '/api/placeholder/400/400'}
|
src={WildberriesService.getCardImages(selectedCardForDetails)[currentImageIndex] || '/api/placeholder/400/400'}
|
||||||
alt={selectedCardForDetails.title}
|
alt={selectedCardForDetails.title}
|
||||||
className="w-full aspect-square rounded-lg object-cover"
|
className="w-full aspect-square rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Навигация по изображениям */}
|
{/* Навигация по изображениям */}
|
||||||
{selectedCardForDetails.mediaFiles?.length > 1 && (
|
{WildberriesService.getCardImages(selectedCardForDetails).length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={prevImage}
|
onClick={prevImage}
|
||||||
@ -1223,17 +1229,17 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 bg-black/70 px-3 py-1 rounded-full text-white text-sm">
|
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 bg-black/70 px-3 py-1 rounded-full text-white text-sm">
|
||||||
{currentImageIndex + 1} из {selectedCardForDetails.mediaFiles?.length || 0}
|
{currentImageIndex + 1} из {WildberriesService.getCardImages(selectedCardForDetails).length}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Миниатюры изображений */}
|
{/* Миниатюры изображений */}
|
||||||
{selectedCardForDetails.mediaFiles?.length > 1 && (
|
{WildberriesService.getCardImages(selectedCardForDetails).length > 1 && (
|
||||||
<div className="w-full overflow-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
<div className="flex space-x-2 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
|
<div className="flex space-x-2 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
|
||||||
{selectedCardForDetails.mediaFiles?.map((image, index) => (
|
{WildberriesService.getCardImages(selectedCardForDetails).map((image, index) => (
|
||||||
<img
|
<img
|
||||||
key={index}
|
key={index}
|
||||||
src={image}
|
src={image}
|
||||||
|
@ -13,23 +13,58 @@ interface WildberriesWarehousesResponse {
|
|||||||
|
|
||||||
interface WildberriesCard {
|
interface WildberriesCard {
|
||||||
nmID: number
|
nmID: number
|
||||||
|
imtID?: number
|
||||||
|
nmUUID?: string
|
||||||
|
subjectID?: number
|
||||||
|
subjectName?: string
|
||||||
vendorCode: string
|
vendorCode: string
|
||||||
|
brand: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
needKiz?: boolean
|
||||||
|
photos?: Array<{
|
||||||
|
big: string
|
||||||
|
c246x328: string
|
||||||
|
c516x688: string
|
||||||
|
square: string
|
||||||
|
tm: string
|
||||||
|
}>
|
||||||
|
video?: string
|
||||||
|
dimensions?: {
|
||||||
|
length: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
weightBrutto: number
|
||||||
|
isValid: boolean
|
||||||
|
}
|
||||||
|
characteristics?: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
value: string[]
|
||||||
|
}>
|
||||||
sizes: Array<{
|
sizes: Array<{
|
||||||
chrtID: number
|
chrtID: number
|
||||||
techSize: string
|
techSize: string
|
||||||
|
skus?: string[]
|
||||||
|
// Legacy fields for backward compatibility
|
||||||
wbSize: string
|
wbSize: string
|
||||||
price: number
|
price: number
|
||||||
discountedPrice: number
|
discountedPrice: number
|
||||||
quantity: number
|
quantity: number
|
||||||
}>
|
}>
|
||||||
|
tags?: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
}>
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
// Legacy fields for backward compatibility
|
||||||
mediaFiles: string[]
|
mediaFiles: string[]
|
||||||
object: string
|
object?: string
|
||||||
parent: string
|
parent?: string
|
||||||
countryProduction: string
|
countryProduction?: string
|
||||||
supplierVendorCode: string
|
supplierVendorCode?: string
|
||||||
brand: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WildberriesCardsResponse {
|
interface WildberriesCardsResponse {
|
||||||
@ -224,6 +259,7 @@ class WildberriesService {
|
|||||||
private apiKey: string
|
private apiKey: string
|
||||||
private baseURL = 'https://statistics-api.wildberries.ru'
|
private baseURL = 'https://statistics-api.wildberries.ru'
|
||||||
private advertURL = 'https://advert-api.wildberries.ru'
|
private advertURL = 'https://advert-api.wildberries.ru'
|
||||||
|
private contentURL = 'https://content-api.wildberries.ru'
|
||||||
|
|
||||||
constructor(apiKey: string) {
|
constructor(apiKey: string) {
|
||||||
this.apiKey = apiKey
|
this.apiKey = apiKey
|
||||||
@ -366,32 +402,93 @@ class WildberriesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Получение карточек товаров
|
// Получение карточек товаров
|
||||||
async getCards(options: { limit?: number; offset?: number } = {}): Promise<WildberriesCard[]> {
|
async getCards(options: { limit?: number; cursor?: { updatedAt?: string; nmID?: number } } = {}): Promise<WildberriesCardsResponse> {
|
||||||
const { limit = 100, offset = 0 } = options
|
const { limit = 100, cursor } = options
|
||||||
const url = `${this.baseURL}/content/v1/cards/cursor/list?sort=updateAt&limit=${limit}&cursor=${offset}`
|
const url = `${this.contentURL}/content/v2/get/cards/list`
|
||||||
console.log(`WB API: Getting cards from ${url}`)
|
|
||||||
|
const body = {
|
||||||
|
settings: {
|
||||||
|
cursor: {
|
||||||
|
limit,
|
||||||
|
...(cursor?.updatedAt && { updatedAt: cursor.updatedAt }),
|
||||||
|
...(cursor?.nmID && { nmID: cursor.nmID })
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
withPhoto: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`WB API: Getting cards from ${url}`, body)
|
||||||
try {
|
try {
|
||||||
const response = await this.makeRequest<{ cards: WildberriesCard[] }>(url)
|
const response = await this.makeRequest<WildberriesCardsResponse>(url, {
|
||||||
return response?.cards || []
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Преобразуем карточки для обратной совместимости
|
||||||
|
const processedCards = response.cards.map(this.processCard)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
cards: processedCards
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`WB API: Error getting cards:`, error)
|
console.error(`WB API: Error getting cards:`, error)
|
||||||
return []
|
return { cards: [], cursor: { total: 0, updatedAt: '', limit: 0, nmID: 0 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка карточки для обратной совместимости
|
||||||
|
private processCard(card: WildberriesCard): WildberriesCard {
|
||||||
|
// Создаем массив URL изображений для совместимости с mediaFiles
|
||||||
|
const mediaFiles: string[] = []
|
||||||
|
|
||||||
|
if (card.photos && card.photos.length > 0) {
|
||||||
|
card.photos.forEach(photo => {
|
||||||
|
// Добавляем разные размеры изображений, приоритет большим размерам
|
||||||
|
if (photo.big) mediaFiles.push(photo.big)
|
||||||
|
if (photo.c516x688) mediaFiles.push(photo.c516x688)
|
||||||
|
if (photo.c246x328) mediaFiles.push(photo.c246x328)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заполняем размеры с ценами и количеством для совместимости
|
||||||
|
const processedSizes = card.sizes.map(size => ({
|
||||||
|
...size,
|
||||||
|
wbSize: size.wbSize || size.techSize || '',
|
||||||
|
price: size.price || 0,
|
||||||
|
discountedPrice: size.discountedPrice || size.price || 0,
|
||||||
|
quantity: size.quantity || 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
...card,
|
||||||
|
// Добавляем mediaFiles для обратной совместимости
|
||||||
|
mediaFiles,
|
||||||
|
// Заполняем legacy поля если они отсутствуют
|
||||||
|
object: card.object || card.subjectName || '',
|
||||||
|
parent: card.parent || '',
|
||||||
|
countryProduction: card.countryProduction || '',
|
||||||
|
supplierVendorCode: card.supplierVendorCode || card.vendorCode,
|
||||||
|
// Обработанные размеры
|
||||||
|
sizes: processedSizes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Поиск карточек товаров
|
// Поиск карточек товаров
|
||||||
async searchCards(searchTerm: string, limit = 100): Promise<WildberriesCard[]> {
|
async searchCards(searchTerm: string, limit = 100): Promise<WildberriesCard[]> {
|
||||||
// Для простоты пока используем тот же API что и getCards
|
// Сначала получаем все карточки
|
||||||
// В реальности может потребоваться другой endpoint для поиска
|
const response = await this.getCards({ limit })
|
||||||
const cards = await this.getCards({ limit })
|
|
||||||
|
|
||||||
// Фильтруем результаты по поисковому запросу
|
// Фильтруем результаты по поисковому запросу
|
||||||
const filteredCards = cards.filter(card => {
|
const filteredCards = response.cards.filter((card: WildberriesCard) => {
|
||||||
const searchLower = searchTerm.toLowerCase()
|
const searchLower = searchTerm.toLowerCase()
|
||||||
return (
|
return (
|
||||||
card.vendorCode?.toLowerCase().includes(searchLower) ||
|
card.vendorCode?.toLowerCase().includes(searchLower) ||
|
||||||
card.object?.toLowerCase().includes(searchLower) ||
|
card.object?.toLowerCase().includes(searchLower) ||
|
||||||
card.brand?.toLowerCase().includes(searchLower)
|
card.brand?.toLowerCase().includes(searchLower) ||
|
||||||
|
card.title?.toLowerCase().includes(searchLower)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -684,10 +781,50 @@ class WildberriesService {
|
|||||||
return service.getWarehouses()
|
return service.getWarehouses()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получение всех карточек с пагинацией
|
||||||
|
async getAllCardsWithPagination(maxCards = 1000): Promise<WildberriesCard[]> {
|
||||||
|
const allCards: WildberriesCard[] = []
|
||||||
|
let cursor: { updatedAt?: string; nmID?: number } | undefined
|
||||||
|
|
||||||
|
while (allCards.length < maxCards) {
|
||||||
|
const response = await this.getCards({
|
||||||
|
limit: Math.min(100, maxCards - allCards.length),
|
||||||
|
cursor
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.cards || response.cards.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
allCards.push(...response.cards)
|
||||||
|
|
||||||
|
// Если получили меньше чем запрашивали, значит это последняя страница
|
||||||
|
if (response.cards.length < 100) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем курсор для следующего запроса
|
||||||
|
const lastCard = response.cards[response.cards.length - 1]
|
||||||
|
cursor = {
|
||||||
|
updatedAt: response.cursor.updatedAt,
|
||||||
|
nmID: lastCard.nmID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allCards
|
||||||
|
}
|
||||||
|
|
||||||
// Статический метод для получения карточек с токеном
|
// Статический метод для получения карточек с токеном
|
||||||
static async getAllCards(apiKey: string, limit = 100): Promise<WildberriesCard[]> {
|
static async getAllCards(apiKey: string, limit = 100): Promise<WildberriesCard[]> {
|
||||||
const service = new WildberriesService(apiKey)
|
const service = new WildberriesService(apiKey)
|
||||||
return service.getCards({ limit })
|
|
||||||
|
// Если запрашивается больше 100 карточек, используем пагинацию
|
||||||
|
if (limit > 100) {
|
||||||
|
return service.getAllCardsWithPagination(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await service.getCards({ limit })
|
||||||
|
return response.cards
|
||||||
}
|
}
|
||||||
|
|
||||||
// Статический метод для поиска карточек с токеном
|
// Статический метод для поиска карточек с токеном
|
||||||
@ -695,6 +832,29 @@ class WildberriesService {
|
|||||||
const service = new WildberriesService(apiKey)
|
const service = new WildberriesService(apiKey)
|
||||||
return service.searchCards(searchTerm, limit)
|
return service.searchCards(searchTerm, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Утилитные методы для работы с изображениями
|
||||||
|
static getCardImage(card: WildberriesCard, size: 'big' | 'c516x688' | 'c246x328' | 'square' | 'tm' = 'c516x688'): string {
|
||||||
|
if (card.photos && card.photos.length > 0) {
|
||||||
|
return card.photos[0][size] || card.photos[0].big || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback на mediaFiles для старых данных
|
||||||
|
if (card.mediaFiles && card.mediaFiles.length > 0) {
|
||||||
|
return card.mediaFiles[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCardImages(card: WildberriesCard): string[] {
|
||||||
|
if (card.photos && card.photos.length > 0) {
|
||||||
|
return card.photos.map(photo => photo.big || photo.c516x688 || photo.c246x328)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback на mediaFiles для старых данных
|
||||||
|
return card.mediaFiles || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { WildberriesService }
|
export { WildberriesService }
|
@ -1,22 +1,57 @@
|
|||||||
export interface WildberriesCard {
|
export interface WildberriesCard {
|
||||||
nmID: number
|
nmID: number
|
||||||
|
imtID?: number
|
||||||
|
nmUUID?: string
|
||||||
|
subjectID?: number
|
||||||
|
subjectName?: string
|
||||||
vendorCode: string
|
vendorCode: string
|
||||||
|
brand: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
needKiz?: boolean
|
||||||
|
photos?: Array<{
|
||||||
|
big: string
|
||||||
|
c246x328: string
|
||||||
|
c516x688: string
|
||||||
|
square: string
|
||||||
|
tm: string
|
||||||
|
}>
|
||||||
|
video?: string
|
||||||
|
dimensions?: {
|
||||||
|
length: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
weightBrutto: number
|
||||||
|
isValid: boolean
|
||||||
|
}
|
||||||
|
characteristics?: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
value: string[]
|
||||||
|
}>
|
||||||
sizes: Array<{
|
sizes: Array<{
|
||||||
chrtID: number
|
chrtID: number
|
||||||
techSize: string
|
techSize: string
|
||||||
|
skus?: string[]
|
||||||
|
// Legacy fields for backward compatibility
|
||||||
wbSize: string
|
wbSize: string
|
||||||
price: number
|
price: number
|
||||||
discountedPrice: number
|
discountedPrice: number
|
||||||
quantity: number
|
quantity: number
|
||||||
}>
|
}>
|
||||||
|
tags?: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
}>
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
// Legacy fields for backward compatibility
|
||||||
mediaFiles: string[]
|
mediaFiles: string[]
|
||||||
object: string
|
object?: string
|
||||||
parent: string
|
parent?: string
|
||||||
countryProduction: string
|
countryProduction?: string
|
||||||
supplierVendorCode: string
|
supplierVendorCode?: string
|
||||||
brand: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectedCard {
|
export interface SelectedCard {
|
||||||
|
Reference in New Issue
Block a user