diff --git a/src/components/market/market-counterparties.tsx b/src/components/market/market-counterparties.tsx index 0fb6eed..2d6a91f 100644 --- a/src/components/market/market-counterparties.tsx +++ b/src/components/market/market-counterparties.tsx @@ -1,15 +1,27 @@ "use client" -import React from 'react' +import React, { useState, useMemo } from 'react' import { useQuery, useMutation } from '@apollo/client' import { Button } from '@/components/ui/button' +import { GlassInput } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Users, ArrowUpCircle, - ArrowDownCircle + ArrowDownCircle, + Search, + Filter, + SortAsc, + SortDesc, + Calendar, + Building, + Phone, + Mail, + MapPin, + X } from 'lucide-react' import { OrganizationCard } from './organization-card' import { OrganizationAvatar } from './organization-avatar' @@ -38,7 +50,15 @@ interface CounterpartyRequest { receiver: Organization } +type SortField = 'name' | 'date' | 'inn' | 'type' +type SortOrder = 'asc' | 'desc' + export function MarketCounterparties() { + const [searchQuery, setSearchQuery] = useState('') + const [typeFilter, setTypeFilter] = useState('all') + const [sortField, setSortField] = useState('name') + const [sortOrder, setSortOrder] = useState('asc') + const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES) const { data: incomingData, loading: incomingLoading } = useQuery(GET_INCOMING_REQUESTS) const { data: outgoingData, loading: outgoingLoading } = useQuery(GET_OUTGOING_REQUESTS) @@ -77,6 +97,56 @@ export function MarketCounterparties() { 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) => { try { 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 incomingRequests = incomingData?.incomingRequests || [] 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 (
-
- -
-

Мои контрагенты

-

Управление контрагентами и заявками

-
-
-
@@ -180,33 +271,200 @@ export function MarketCounterparties() { - - {counterpartiesLoading ? ( -
-
Загрузка...
-
- ) : counterparties.length === 0 ? ( -
-
- -

У вас пока нет контрагентов

-

- Перейдите на другие вкладки, чтобы найти партнеров -

+ + {/* Компактная панель фильтров */} +
+
+ {/* Поиск */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 h-9" + /> +
+
+ + {/* Фильтры и сортировка */} +
+ + + + + + + {hasActiveFilters && ( + + )}
- ) : ( -
- {counterparties.map((organization: Organization) => ( - - ))} + + {/* Статистика и быстрые фильтры */} +
+
+ {filteredAndSortedCounterparties.length} из {counterparties.length} +
+ +
+ {['FULFILLMENT', 'SELLER', 'LOGIST', 'WHOLESALE'].map((type) => { + const count = counterparties.filter((org: Organization) => org.type === type).length + if (count === 0) return null + + return ( + + ) + })} +
- )} +
+ + {/* Список контрагентов */} +
+ {counterpartiesLoading ? ( +
+
Загрузка...
+
+ ) : filteredAndSortedCounterparties.length === 0 ? ( +
+
+ {counterparties.length === 0 ? ( + <> + +

У вас пока нет контрагентов

+

+ Перейдите на другие вкладки, чтобы найти партнеров +

+ + ) : ( + <> + +

Ничего не найдено

+

+ Попробуйте изменить параметры поиска или фильтрации +

+ + )} +
+
+ ) : ( +
+ {filteredAndSortedCounterparties.map((organization: Organization) => ( +
+
+
+ +
+
+

+ {organization.name || organization.fullName} +

+
+ + {getTypeLabel(organization.type)} + +
+
+ +
+
+ + ИНН: {organization.inn} +
+ + {organization.address && ( +
+ + {organization.address} +
+ )} + + {organization.phones && organization.phones.length > 0 && ( +
+ + {organization.phones[0].value} +
+ )} + + {organization.emails && organization.emails.length > 0 && ( +
+ + {organization.emails[0].value} +
+ )} + +
+ + Добавлен {formatDate(organization.createdAt)} +
+
+
+
+ + +
+
+ ))} +
+ )} +
@@ -234,18 +492,8 @@ export function MarketCounterparties() { {request.sender.name || request.sender.fullName}
- - {request.sender.type === 'FULFILLMENT' ? 'Фулфилмент' : - request.sender.type === 'SELLER' ? 'Селлер' : - request.sender.type === 'LOGIST' ? 'Логистика' : - request.sender.type === 'WHOLESALE' ? 'Оптовик' : - request.sender.type} + + {getTypeLabel(request.sender.type)}
@@ -318,18 +566,8 @@ export function MarketCounterparties() { {request.receiver.name || request.receiver.fullName}
- - {request.receiver.type === 'FULFILLMENT' ? 'Фулфилмент' : - request.receiver.type === 'SELLER' ? 'Селлер' : - request.receiver.type === 'LOGIST' ? 'Логистика' : - request.receiver.type === 'WHOLESALE' ? 'Оптовик' : - request.receiver.type} + + {getTypeLabel(request.receiver.type)} { - if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) { - setCurrentImageIndex((prev) => (prev + 1) % selectedCardForDetails.mediaFiles.length) + if (selectedCardForDetails) { + const images = WildberriesService.getCardImages(selectedCardForDetails) + if (images.length > 1) { + setCurrentImageIndex((prev) => (prev + 1) % images.length) + } } } const prevImage = () => { - if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) { - setCurrentImageIndex((prev) => (prev - 1 + selectedCardForDetails.mediaFiles.length) % selectedCardForDetails.mediaFiles.length) + if (selectedCardForDetails) { + 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) {
{sc.card.title} @@ -1017,7 +1023,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
{card.title} handleCardClick(card)} @@ -1201,13 +1207,13 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
{selectedCardForDetails.title} {/* Навигация по изображениям */} - {selectedCardForDetails.mediaFiles?.length > 1 && ( + {WildberriesService.getCardImages(selectedCardForDetails).length > 1 && ( <>
{/* Миниатюры изображений */} - {selectedCardForDetails.mediaFiles?.length > 1 && ( + {WildberriesService.getCardImages(selectedCardForDetails).length > 1 && (
- {selectedCardForDetails.mediaFiles?.map((image, index) => ( + {WildberriesService.getCardImages(selectedCardForDetails).map((image, index) => ( + video?: string + dimensions?: { + length: number + width: number + height: number + weightBrutto: number + isValid: boolean + } + characteristics?: Array<{ + id: number + name: string + value: string[] + }> sizes: Array<{ chrtID: number techSize: string + skus?: string[] + // Legacy fields for backward compatibility wbSize: string price: number discountedPrice: number quantity: number }> + tags?: Array<{ + id: number + name: string + color: string + }> + createdAt?: string + updatedAt?: string + // Legacy fields for backward compatibility mediaFiles: string[] - object: string - parent: string - countryProduction: string - supplierVendorCode: string - brand: string - title: string - description: string + object?: string + parent?: string + countryProduction?: string + supplierVendorCode?: string } interface WildberriesCardsResponse { @@ -224,6 +259,7 @@ class WildberriesService { private apiKey: string private baseURL = 'https://statistics-api.wildberries.ru' private advertURL = 'https://advert-api.wildberries.ru' + private contentURL = 'https://content-api.wildberries.ru' constructor(apiKey: string) { this.apiKey = apiKey @@ -366,32 +402,93 @@ class WildberriesService { } // Получение карточек товаров - async getCards(options: { limit?: number; offset?: number } = {}): Promise { - const { limit = 100, offset = 0 } = options - const url = `${this.baseURL}/content/v1/cards/cursor/list?sort=updateAt&limit=${limit}&cursor=${offset}` - console.log(`WB API: Getting cards from ${url}`) + async getCards(options: { limit?: number; cursor?: { updatedAt?: string; nmID?: number } } = {}): Promise { + const { limit = 100, cursor } = options + const url = `${this.contentURL}/content/v2/get/cards/list` + + 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 { - const response = await this.makeRequest<{ cards: WildberriesCard[] }>(url) - return response?.cards || [] + const response = await this.makeRequest(url, { + method: 'POST', + body: JSON.stringify(body) + }) + + // Преобразуем карточки для обратной совместимости + const processedCards = response.cards.map(this.processCard) + + return { + ...response, + cards: processedCards + } } catch (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 { - // Для простоты пока используем тот же API что и getCards - // В реальности может потребоваться другой endpoint для поиска - const cards = await this.getCards({ limit }) + // Сначала получаем все карточки + const response = await this.getCards({ limit }) // Фильтруем результаты по поисковому запросу - const filteredCards = cards.filter(card => { + const filteredCards = response.cards.filter((card: WildberriesCard) => { const searchLower = searchTerm.toLowerCase() return ( card.vendorCode?.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() } + // Получение всех карточек с пагинацией + async getAllCardsWithPagination(maxCards = 1000): Promise { + 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 { 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) 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 } \ No newline at end of file diff --git a/src/types/supplies.ts b/src/types/supplies.ts index 4da63a7..5c2c4d4 100644 --- a/src/types/supplies.ts +++ b/src/types/supplies.ts @@ -1,22 +1,57 @@ export interface WildberriesCard { nmID: number + imtID?: number + nmUUID?: string + subjectID?: number + subjectName?: 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<{ chrtID: number techSize: string + skus?: string[] + // Legacy fields for backward compatibility wbSize: string price: number discountedPrice: number quantity: number }> + tags?: Array<{ + id: number + name: string + color: string + }> + createdAt?: string + updatedAt?: string + // Legacy fields for backward compatibility mediaFiles: string[] - object: string - parent: string - countryProduction: string - supplierVendorCode: string - brand: string - title: string - description: string + object?: string + parent?: string + countryProduction?: string + supplierVendorCode?: string } export interface SelectedCard {