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.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.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
*/