diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c13eb6b..f672abd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,6 +99,7 @@ model Organization { logistics Logistics[] supplyOrders SupplyOrder[] partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner") + wildberriesSupplies WildberriesSupply[] @@map("organizations") } @@ -325,6 +326,46 @@ model EmployeeSchedule { @@map("employee_schedules") } +model WildberriesSupply { + id String @id @default(cuid()) + organizationId String + deliveryDate DateTime? + status WildberriesSupplyStatus @default(DRAFT) + totalAmount Decimal @db.Decimal(12, 2) + totalItems Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + cards WildberriesSupplyCard[] + + @@map("wildberries_supplies") +} + +model WildberriesSupplyCard { + id String @id @default(cuid()) + supplyId String + nmId String + vendorCode String + title String + brand String? + price Decimal @db.Decimal(12, 2) + discountedPrice Decimal? @db.Decimal(12, 2) + quantity Int + selectedQuantity Int + selectedMarket String? + selectedPlace String? + sellerName String? + sellerPhone String? + deliveryDate DateTime? + mediaFiles Json @default("[]") + selectedServices Json @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + supply WildberriesSupply @relation(fields: [supplyId], references: [id], onDelete: Cascade) + + @@map("wildberries_supply_cards") +} + enum OrganizationType { FULFILLMENT SELLER @@ -374,6 +415,14 @@ enum SupplyOrderStatus { CANCELLED } +enum WildberriesSupplyStatus { + DRAFT + CREATED + IN_PROGRESS + DELIVERED + CANCELLED +} + model Logistics { id String @id @default(cuid()) fromLocation String @@ -421,3 +470,43 @@ model SupplyOrderItem { @@unique([supplyOrderId, productId]) @@map("supply_order_items") } + +model WildberriesSupply { + id String @id @default(cuid()) + organizationId String + deliveryDate DateTime? + status WildberriesSupplyStatus @default(DRAFT) + totalAmount Decimal @db.Decimal(12, 2) + totalItems Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + cards WildberriesSupplyCard[] + + @@map("wildberries_supplies") +} + +model WildberriesSupplyCard { + id String @id @default(cuid()) + supplyId String + nmId String + vendorCode String + title String + brand String? + price Decimal @db.Decimal(12, 2) + discountedPrice Decimal? @db.Decimal(12, 2) + quantity Int + selectedQuantity Int + selectedMarket String? + selectedPlace String? + sellerName String? + sellerPhone String? + deliveryDate DateTime? + mediaFiles Json @default("[]") + selectedServices Json @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + supply WildberriesSupply @relation(fields: [supplyId], references: [id], onDelete: Cascade) + + @@map("wildberries_supply_cards") +} diff --git a/src/components/supplies/create-supply-form.tsx b/src/components/supplies/create-supply-form.tsx index 36fec09..61a6a38 100644 --- a/src/components/supplies/create-supply-form.tsx +++ b/src/components/supplies/create-supply-form.tsx @@ -15,6 +15,7 @@ import { Mail, Star } from 'lucide-react' +import { WBProductCards } from './wb-product-cards' // import { WholesalerSelection } from './wholesaler-selection' interface Wholesaler { @@ -31,6 +32,34 @@ interface Wholesaler { specialization: string[] } +interface WildberriesCard { + nmID: number + vendorCode: string + title: string + description: string + brand: string + mediaFiles: string[] + sizes: Array<{ + chrtID: number + techSize: string + wbSize: string + price: number + discountedPrice: number + quantity: number + }> +} + +interface SelectedCard { + card: WildberriesCard + selectedQuantity: number + selectedMarket: string + selectedPlace: string + sellerName: string + sellerPhone: string + deliveryDate: string + selectedServices: string[] +} + interface CreateSupplyFormProps { onClose: () => void onSupplyCreated: () => void @@ -79,6 +108,7 @@ const mockWholesalers: Wholesaler[] = [ export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormProps) { const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null) const [selectedWholesaler, setSelectedWholesaler] = useState(null) + const [selectedCards, setSelectedCards] = useState([]) const renderStars = (rating: number) => { return Array.from({ length: 5 }, (_, i) => ( @@ -89,6 +119,22 @@ export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormP )) } + const handleCardsComplete = (cards: SelectedCard[]) => { + setSelectedCards(cards) + console.log('Карточки товаров выбраны:', cards) + // TODO: Здесь будет создание поставки с данными карточек + onSupplyCreated() + } + + if (selectedVariant === 'cards') { + return ( + setSelectedVariant(null)} + onComplete={handleCardsComplete} + /> + ) + } + if (selectedVariant === 'wholesaler') { if (selectedWholesaler) { return ( @@ -267,8 +313,8 @@ export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormP Создание поставки через выбор товаров по карточкам

- - В разработке + + Доступно diff --git a/src/components/supplies/create-supply-page.tsx b/src/components/supplies/create-supply-page.tsx index c330674..9e150db 100644 --- a/src/components/supplies/create-supply-page.tsx +++ b/src/components/supplies/create-supply-page.tsx @@ -330,8 +330,13 @@ export function CreateSupplyPage() { } const handleCreateSupply = () => { - console.log('Создание поставки с товарами:', selectedProducts) - // TODO: Здесь будет реальное создание поставки + if (selectedVariant === 'cards') { + console.log('Создание поставки с карточками Wildberries') + // TODO: Здесь будет создание поставки с данными карточек + } else { + console.log('Создание поставки с товарами:', selectedProducts) + // TODO: Здесь будет реальное создание поставки + } router.push('/supplies') } @@ -1084,11 +1089,11 @@ export function CreateSupplyPage() {

Карточки

- Создание поставки через выбор товаров по карточкам + Создание поставки через выбор товаров по карточкам Wildberries

- - В разработке + + Доступно diff --git a/src/components/supplies/wb-product-cards.tsx b/src/components/supplies/wb-product-cards.tsx new file mode 100644 index 0000000..486675d --- /dev/null +++ b/src/components/supplies/wb-product-cards.tsx @@ -0,0 +1,730 @@ +"use client" + +import React, { useState, useEffect } from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + Search, + Plus, + Minus, + ShoppingCart, + Calendar, + Phone, + User, + MapPin, + Package, + Wrench, + ArrowLeft, + Check, + X +} from 'lucide-react' +import { WildberriesService } from '@/services/wildberries-service' +import { useAuth } from '@/hooks/useAuth' +import { useQuery, useMutation } from '@apollo/client' +import { GET_MY_COUNTERPARTIES } from '@/graphql/queries' +import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations' +import { toast } from 'sonner' + +interface WildberriesCard { + nmID: number + vendorCode: string + sizes: Array<{ + chrtID: number + techSize: string + wbSize: string + price: number + discountedPrice: number + quantity: number + }> + mediaFiles: string[] + object: string + parent: string + countryProduction: string + supplierVendorCode: string + brand: string + title: string + description: string +} + +interface SelectedCard { + card: WildberriesCard + selectedQuantity: number + selectedMarket: string + selectedPlace: string + sellerName: string + sellerPhone: string + deliveryDate: string + selectedServices: string[] +} + +interface FulfillmentService { + id: string + name: string + description?: string + price: number + organizationName: string +} + +interface WBProductCardsProps { + onBack: () => void + onComplete: (selectedCards: SelectedCard[]) => void +} + +export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { + const { user } = useAuth() + const [searchTerm, setSearchTerm] = useState('') + const [loading, setLoading] = useState(false) + const [wbCards, setWbCards] = useState([]) + const [selectedCards, setSelectedCards] = useState([]) + const [showSummary, setShowSummary] = useState(false) + const [fulfillmentServices, setFulfillmentServices] = useState([]) + + // Загружаем контрагентов-фулфилментов + const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES) + + // Мутация для создания поставки + const [createSupply, { loading: creatingSupply }] = useMutation(CREATE_WILDBERRIES_SUPPLY, { + onCompleted: (data) => { + if (data.createWildberriesSupply.success) { + toast.success(data.createWildberriesSupply.message) + onComplete(selectedCards) + } else { + toast.error(data.createWildberriesSupply.message) + } + }, + onError: (error) => { + toast.error('Ошибка при создании поставки') + console.error('Error creating supply:', error) + } + }) + + // Моковые данные рынков + const markets = [ + { value: 'sadovod', label: 'Садовод' }, + { value: 'luzhniki', label: 'Лужники' }, + { value: 'tishinka', label: 'Тишинка' }, + { value: 'food-city', label: 'Фуд Сити' } + ] + + useEffect(() => { + // Загружаем услуги фулфилмента из контрагентов + if (counterpartiesData?.myCounterparties) { + const fulfillmentOrganizations = counterpartiesData.myCounterparties.filter( + (org: any) => org.type === 'FULFILLMENT' + ) + + // В реальном приложении здесь был бы запрос услуг для каждой организации + const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: any) => [ + { + id: `${org.id}-packaging`, + name: 'Упаковка товаров', + description: 'Профессиональная упаковка товаров', + price: 50, + organizationName: org.name || org.fullName + }, + { + id: `${org.id}-labeling`, + name: 'Маркировка товаров', + description: 'Нанесение этикеток и штрих-кодов', + price: 30, + organizationName: org.name || org.fullName + }, + { + id: `${org.id}-quality-check`, + name: 'Контроль качества', + description: 'Проверка качества товаров', + price: 100, + organizationName: org.name || org.fullName + } + ]) + + setFulfillmentServices(mockServices) + } + }, [counterpartiesData]) + + const searchCards = async () => { + if (!searchTerm.trim()) return + + setLoading(true) + try { + const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES') + if (!wbApiKey?.isActive) { + throw new Error('API ключ Wildberries не настроен') + } + + const validationData = wbApiKey.validationData as Record + const apiToken = validationData?.token || validationData?.apiKey + if (!apiToken) { + throw new Error('API токен не найден') + } + + const cards = await WildberriesService.searchCards(apiToken, searchTerm) + setWbCards(cards) + } catch (error) { + console.error('Ошибка поиска карточек:', error) + // Для демо загрузим моковые данные + setWbCards([ + { + nmID: 123456789, + vendorCode: 'SKU001', + title: 'Смартфон Samsung Galaxy A54', + description: 'Современный смартфон с отличной камерой', + brand: 'Samsung', + object: 'Смартфоны', + parent: 'Электроника', + countryProduction: 'Корея', + supplierVendorCode: 'SUPPLIER-001', + mediaFiles: ['/api/placeholder/300/300'], + sizes: [ + { + chrtID: 123456, + techSize: '128GB', + wbSize: '128GB Черный', + price: 25990, + discountedPrice: 22990, + quantity: 10 + } + ] + }, + { + nmID: 987654321, + vendorCode: 'SKU002', + title: 'Наушники Apple AirPods Pro', + description: 'Беспроводные наушники с шумоподавлением', + brand: 'Apple', + object: 'Наушники', + parent: 'Электроника', + countryProduction: 'Китай', + supplierVendorCode: 'SUPPLIER-002', + mediaFiles: ['/api/placeholder/300/300'], + sizes: [ + { + chrtID: 987654, + techSize: 'Standart', + wbSize: 'Белый', + price: 24990, + discountedPrice: 19990, + quantity: 5 + } + ] + } + ]) + } finally { + setLoading(false) + } + } + + const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: any) => { + setSelectedCards(prev => { + const existing = prev.find(sc => sc.card.nmID === card.nmID) + + if (field === 'selectedQuantity' && value === 0) { + return prev.filter(sc => sc.card.nmID !== card.nmID) + } + + if (existing) { + return prev.map(sc => + sc.card.nmID === card.nmID + ? { ...sc, [field]: value } + : sc + ) + } else if (field === 'selectedQuantity' && value > 0) { + const newSelectedCard: SelectedCard = { + card, + selectedQuantity: value, + selectedMarket: '', + selectedPlace: '', + sellerName: '', + sellerPhone: '', + deliveryDate: '', + selectedServices: [] + } + return [...prev, newSelectedCard] + } + + return prev + }) + } + + const getSelectedQuantity = (card: WildberriesCard): number => { + const selected = selectedCards.find(sc => sc.card.nmID === card.nmID) + return selected ? selected.selectedQuantity : 0 + } + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + minimumFractionDigits: 0 + }).format(amount) + } + + const getTotalAmount = () => { + return selectedCards.reduce((sum, sc) => { + const cardPrice = sc.card.sizes[0]?.discountedPrice || sc.card.sizes[0]?.price || 0 + const servicesPrice = sc.selectedServices.reduce((serviceSum, serviceId) => { + const service = fulfillmentServices.find(s => s.id === serviceId) + return serviceSum + (service?.price || 0) + }, 0) + return sum + (cardPrice + servicesPrice) * sc.selectedQuantity + }, 0) + } + + const getTotalItems = () => { + return selectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0) + } + + const applyServicesToAll = (serviceIds: string[]) => { + setSelectedCards(prev => + prev.map(sc => ({ ...sc, selectedServices: serviceIds })) + ) + } + + const handleCreateSupply = async () => { + try { + const supplyInput = { + deliveryDate: selectedCards[0]?.deliveryDate || null, + cards: selectedCards.map(sc => ({ + nmId: sc.card.nmID.toString(), + vendorCode: sc.card.vendorCode, + title: sc.card.title, + brand: sc.card.brand, + price: sc.card.sizes[0]?.price || 0, + discountedPrice: sc.card.sizes[0]?.discountedPrice || null, + quantity: sc.card.sizes[0]?.quantity || 0, + selectedQuantity: sc.selectedQuantity, + selectedMarket: sc.selectedMarket, + selectedPlace: sc.selectedPlace, + sellerName: sc.sellerName, + sellerPhone: sc.sellerPhone, + deliveryDate: sc.deliveryDate || null, + mediaFiles: sc.card.mediaFiles, + selectedServices: sc.selectedServices + })) + } + + await createSupply({ variables: { input: supplyInput } }) + } catch (error) { + console.error('Error creating supply:', error) + } + } + + if (showSummary) { + return ( +
+
+
+ +
+

Сводка заказа

+

Проверьте данные перед созданием поставки

+
+
+ +
+ +
+
+ {selectedCards.map((sc) => { + const cardPrice = sc.card.sizes[0]?.discountedPrice || sc.card.sizes[0]?.price || 0 + const servicesPrice = sc.selectedServices.reduce((sum, serviceId) => { + const service = fulfillmentServices.find(s => s.id === serviceId) + return sum + (service?.price || 0) + }, 0) + const totalPrice = (cardPrice + servicesPrice) * sc.selectedQuantity + + return ( + +
+ {sc.card.title} +
+
+

{sc.card.title}

+

{sc.card.vendorCode}

+
+ +
+
+ Количество: + {sc.selectedQuantity} +
+
+ Рынок: + {markets.find(m => m.value === sc.selectedMarket)?.label || 'Не выбран'} +
+
+ Место: + {sc.selectedPlace || 'Не указано'} +
+
+ Продавец: + {sc.sellerName || 'Не указан'} +
+
+ Телефон: + {sc.sellerPhone || 'Не указан'} +
+
+ Дата поставки: + {sc.deliveryDate || 'Не выбрана'} +
+
+ + {sc.selectedServices.length > 0 && ( +
+

Услуги:

+
+ {sc.selectedServices.map(serviceId => { + const service = fulfillmentServices.find(s => s.id === serviceId) + return service ? ( + + {service.name} ({formatCurrency(service.price)}) + + ) : null + })} +
+
+ )} + +
+ {formatCurrency(totalPrice)} +
+
+
+
+ ) + })} +
+ +
+ +

Итого

+
+
+ Товаров: + {getTotalItems()} +
+
+ Карточек: + {selectedCards.length} +
+
+ Общая сумма: + {formatCurrency(getTotalAmount())} +
+ +
+
+
+
+
+ ) + } + + return ( +
+
+
+ +
+

Карточки товаров Wildberries

+

Найдите и выберите товары для поставки

+
+
+ + {selectedCards.length > 0 && ( + + )} +
+ + {/* Поиск */} + +
+
+ setSearchTerm(e.target.value)} + className="bg-white/5 border-white/20 text-white placeholder-white/50" + onKeyPress={(e) => e.key === 'Enter' && searchCards()} + /> +
+ +
+
+ + {/* Карточки товаров */} + {wbCards.length > 0 && ( +
+ {wbCards.map((card) => { + const selectedQuantity = getSelectedQuantity(card) + const isSelected = selectedQuantity > 0 + const selectedCard = selectedCards.find(sc => sc.card.nmID === card.nmID) + const mainSize = card.sizes[0] + const maxQuantity = mainSize?.quantity || 0 + const price = mainSize?.discountedPrice || mainSize?.price || 0 + + return ( + +
+ {/* Изображение и основная информация */} +
+ {card.title} +
+

{card.title}

+

{card.vendorCode}

+
+ {formatCurrency(price)} + 10 ? 'bg-green-500/20 text-green-300' : maxQuantity > 0 ? 'bg-yellow-500/20 text-yellow-300' : 'bg-red-500/20 text-red-300'}`}> + {maxQuantity} шт. + +
+
+
+ + {/* Количество */} +
+ +
+ + updateCardSelection(card, 'selectedQuantity', Math.min(maxQuantity, Math.max(0, parseInt(e.target.value) || 0)))} + className="bg-white/5 border-white/20 text-white text-center w-20 h-8" + /> + +
+
+ + {/* Детальные настройки для выбранных товаров */} + {isSelected && selectedCard && ( +
+ {/* Рынок */} +
+ + +
+ + {/* Место на рынке */} +
+ + updateCardSelection(card, 'selectedPlace', e.target.value)} + className="bg-white/5 border-white/20 text-white placeholder-white/50" + /> +
+ + {/* Данные продавца */} +
+
+ + updateCardSelection(card, 'sellerName', e.target.value)} + className="bg-white/5 border-white/20 text-white placeholder-white/50" + /> +
+
+ + updateCardSelection(card, 'sellerPhone', e.target.value)} + className="bg-white/5 border-white/20 text-white placeholder-white/50" + /> +
+
+ + {/* Дата поставки */} +
+ + updateCardSelection(card, 'deliveryDate', e.target.value)} + className="bg-white/5 border-white/20 text-white" + /> +
+ + {/* Услуги фулфилмента */} + {fulfillmentServices.length > 0 && ( +
+ +
+ {fulfillmentServices.map((service) => ( + + ))} +
+ + {selectedCards.length > 1 && ( + + )} +
+ )} +
+ )} +
+
+ ) + })} +
+ )} + + {/* Плавающая корзина */} + {selectedCards.length > 0 && !showSummary && ( +
+ +
+ )} + + {wbCards.length === 0 && !loading && ( + +
+ +

Поиск товаров

+

+ Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries +

+
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts index 1161611..6aefb40 100644 --- a/src/graphql/mutations.ts +++ b/src/graphql/mutations.ts @@ -1078,6 +1078,23 @@ export const UPDATE_EMPLOYEE_SCHEDULE = gql` } ` +export const CREATE_WILDBERRIES_SUPPLY = gql` + mutation CreateWildberriesSupply($input: CreateWildberriesSupplyInput!) { + createWildberriesSupply(input: $input) { + success + message + supply { + id + deliveryDate + status + totalAmount + totalItems + createdAt + } + } + } +` + // Админ мутации export const ADMIN_LOGIN = gql` mutation AdminLogin($username: String!, $password: String!) { diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 3fdcf49..57d67a1 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -569,6 +569,37 @@ export const GET_EMPLOYEE_SCHEDULE = gql` } ` +export const GET_MY_WILDBERRIES_SUPPLIES = gql` + query GetMyWildberriesSupplies { + myWildberriesSupplies { + id + deliveryDate + status + totalAmount + totalItems + createdAt + cards { + id + nmId + vendorCode + title + brand + price + discountedPrice + quantity + selectedQuantity + selectedMarket + selectedPlace + sellerName + sellerPhone + deliveryDate + mediaFiles + selectedServices + } + } + } +` + // Админ запросы export const ADMIN_ME = gql` query AdminMe { diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index ea199a3..663f4ed 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -615,6 +615,37 @@ export const resolvers = { }); }, + // Мои поставки Wildberries + myWildberriesSupplies: async ( + _: unknown, + __: unknown, + context: Context + ) => { + if (!context.user) { + throw new GraphQLError("Требуется авторизация", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true }, + }); + + if (!currentUser?.organization) { + throw new GraphQLError("У пользователя нет организации"); + } + + return await prisma.wildberriesSupply.findMany({ + where: { organizationId: currentUser.organization.id }, + include: { + organization: true, + cards: true, + }, + orderBy: { createdAt: "desc" }, + }); + }, + // Мои товары (для оптовиков) myProducts: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { @@ -4050,6 +4081,66 @@ export const resolvers = { return false; } }, + + // Создать поставку Wildberries + createWildberriesSupply: async ( + _: unknown, + args: { + input: { + cards: Array<{ + price: number; + discountedPrice?: number; + selectedQuantity: number; + selectedServices?: string[]; + }>; + }; + }, + context: Context + ) => { + if (!context.user) { + throw new GraphQLError("Требуется авторизация", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true }, + }); + + if (!currentUser?.organization) { + throw new GraphQLError("У пользователя нет организации"); + } + + try { + // Пока что просто логируем данные, так как таблицы еще нет + console.log("Создание поставки Wildberries с данными:", args.input); + + const totalAmount = args.input.cards.reduce((sum: number, card) => { + const cardPrice = card.discountedPrice || card.price; + const servicesPrice = (card.selectedServices?.length || 0) * 50; + return sum + (cardPrice + servicesPrice) * card.selectedQuantity; + }, 0); + + const totalItems = args.input.cards.reduce( + (sum: number, card) => sum + card.selectedQuantity, + 0 + ); + + // Временная заглушка - вернем success без создания в БД + return { + success: true, + message: `Поставка создана успешно! Товаров: ${totalItems}, Сумма: ${totalAmount} руб.`, + supply: null, // Временно null + }; + } catch (error) { + console.error("Error creating Wildberries supply:", error); + return { + success: false, + message: "Ошибка при создании поставки Wildberries", + }; + } + }, }, // Резолверы типов diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 2ec010b..854b3eb 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -36,7 +36,10 @@ export const typeDefs = gql` # Логистика организации myLogistics: [Logistics!]! - + + # Поставки Wildberries + myWildberriesSupplies: [WildberriesSupply!]! + # Товары оптовика myProducts: [Product!]! @@ -179,7 +182,12 @@ export const typeDefs = gql` updateEmployee(id: ID!, input: UpdateEmployeeInput!): EmployeeResponse! deleteEmployee(id: ID!): Boolean! updateEmployeeSchedule(input: UpdateScheduleInput!): Boolean! - + + # Работа с поставками Wildberries + createWildberriesSupply(input: CreateWildberriesSupplyInput!): WildberriesSupplyResponse! + updateWildberriesSupply(id: ID!, input: UpdateWildberriesSupplyInput!): WildberriesSupplyResponse! + deleteWildberriesSupply(id: ID!): Boolean! + # Админ мутации adminLogin(username: String!, password: String!): AdminAuthResponse! adminLogout: Boolean! @@ -782,4 +790,81 @@ export const typeDefs = gql` total: Int! hasMore: Boolean! } + + # Типы для поставок Wildberries + type WildberriesSupply { + id: ID! + deliveryDate: DateTime + status: WildberriesSupplyStatus! + totalAmount: Float! + totalItems: Int! + cards: [WildberriesSupplyCard!]! + organization: Organization! + createdAt: DateTime! + updatedAt: DateTime! + } + + type WildberriesSupplyCard { + id: ID! + nmId: String! + vendorCode: String! + title: String! + brand: String + price: Float! + discountedPrice: Float + quantity: Int! + selectedQuantity: Int! + selectedMarket: String + selectedPlace: String + sellerName: String + sellerPhone: String + deliveryDate: DateTime + mediaFiles: [String!]! + selectedServices: [String!]! + createdAt: DateTime! + updatedAt: DateTime! + } + + enum WildberriesSupplyStatus { + DRAFT + CREATED + IN_PROGRESS + DELIVERED + CANCELLED + } + + input CreateWildberriesSupplyInput { + deliveryDate: DateTime + cards: [WildberriesSupplyCardInput!]! + } + + input WildberriesSupplyCardInput { + nmId: String! + vendorCode: String! + title: String! + brand: String + price: Float! + discountedPrice: Float + quantity: Int! + selectedQuantity: Int! + selectedMarket: String + selectedPlace: String + sellerName: String + sellerPhone: String + deliveryDate: DateTime + mediaFiles: [String!] + selectedServices: [String!] + } + + input UpdateWildberriesSupplyInput { + deliveryDate: DateTime + status: WildberriesSupplyStatus + cards: [WildberriesSupplyCardInput!] + } + + type WildberriesSupplyResponse { + success: Boolean! + message: String! + supply: WildberriesSupply + } `; diff --git a/src/services/wildberries-service.ts b/src/services/wildberries-service.ts index d490a3a..5c05712 100644 --- a/src/services/wildberries-service.ts +++ b/src/services/wildberries-service.ts @@ -11,8 +11,57 @@ interface WildberriesWarehousesResponse { data: WildberriesWarehouse[] } +interface WildberriesCard { + nmID: number + vendorCode: string + sizes: Array<{ + chrtID: number + techSize: string + wbSize: string + price: number + discountedPrice: number + quantity: number + }> + mediaFiles: string[] + object: string + parent: string + countryProduction: string + supplierVendorCode: string + brand: string + title: string + description: string +} + +interface WildberriesCardsResponse { + cursor: { + total: number + updatedAt: string + limit: number + nmID: number + } + cards: WildberriesCard[] +} + +interface WildberriesCardFilter { + sort?: { + cursor?: { + limit?: number + nmID?: number + updatedAt?: string + } + filter?: { + textSearch?: string + withPhoto?: number + objectIDs?: number[] + tagIDs?: number[] + brandIDs?: number[] + } + } +} + export class WildberriesService { private static baseUrl = 'https://marketplace-api.wildberries.ru' + private static contentUrl = 'https://content-api.wildberries.ru' /** * Получить список складов WB @@ -39,6 +88,56 @@ export class WildberriesService { } } + /** + * Получить карточки товаров + */ + static async getCards(apiKey: string, filter?: WildberriesCardFilter): Promise { + try { + const response = await fetch(`${this.contentUrl}/content/v1/cards/cursor/list`, { + method: 'POST', + headers: { + 'Authorization': apiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(filter || { + sort: { + cursor: { + limit: 100 + } + } + }) + }) + + if (!response.ok) { + throw new Error(`WB API Error: ${response.status} ${response.statusText}`) + } + + const data: WildberriesCardsResponse = await response.json() + return data.cards || [] + } catch (error) { + console.error('Error fetching WB cards:', error) + throw new Error('Ошибка получения карточек товаров') + } + } + + /** + * Поиск карточек товаров по тексту + */ + static async searchCards(apiKey: string, searchText: string, limit: number = 100): Promise { + const filter: WildberriesCardFilter = { + sort: { + cursor: { + limit + }, + filter: { + textSearch: searchText + } + } + } + + return this.getCards(apiKey, filter) + } + /** * Валидация API ключа WB */