Добавлены модели WildberriesSupply и WildberriesSupplyCard в схему Prisma, а также соответствующие мутации и запросы в GraphQL для управления поставками Wildberries. Обновлены компоненты создания поставки для поддержки выбора карточек товаров и улучшен интерфейс. Реализована логика обработки карточек товаров и их отображения.
This commit is contained in:
@ -97,6 +97,7 @@ model Organization {
|
|||||||
supplies Supply[]
|
supplies Supply[]
|
||||||
users User[]
|
users User[]
|
||||||
logistics Logistics[]
|
logistics Logistics[]
|
||||||
|
wildberriesSupplies WildberriesSupply[]
|
||||||
|
|
||||||
@@map("organizations")
|
@@map("organizations")
|
||||||
}
|
}
|
||||||
@ -322,6 +323,46 @@ model EmployeeSchedule {
|
|||||||
@@map("employee_schedules")
|
@@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 {
|
enum OrganizationType {
|
||||||
FULFILLMENT
|
FULFILLMENT
|
||||||
SELLER
|
SELLER
|
||||||
@ -363,6 +404,14 @@ enum ScheduleStatus {
|
|||||||
ABSENT
|
ABSENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum WildberriesSupplyStatus {
|
||||||
|
DRAFT
|
||||||
|
CREATED
|
||||||
|
IN_PROGRESS
|
||||||
|
DELIVERED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
model Logistics {
|
model Logistics {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
fromLocation String
|
fromLocation String
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Star
|
Star
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { WBProductCards } from './wb-product-cards'
|
||||||
// import { WholesalerSelection } from './wholesaler-selection'
|
// import { WholesalerSelection } from './wholesaler-selection'
|
||||||
|
|
||||||
interface Wholesaler {
|
interface Wholesaler {
|
||||||
@ -31,6 +32,34 @@ interface Wholesaler {
|
|||||||
specialization: string[]
|
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 {
|
interface CreateSupplyFormProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSupplyCreated: () => void
|
onSupplyCreated: () => void
|
||||||
@ -79,6 +108,7 @@ const mockWholesalers: Wholesaler[] = [
|
|||||||
export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormProps) {
|
export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormProps) {
|
||||||
const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null)
|
const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null)
|
||||||
const [selectedWholesaler, setSelectedWholesaler] = useState<Wholesaler | null>(null)
|
const [selectedWholesaler, setSelectedWholesaler] = useState<Wholesaler | null>(null)
|
||||||
|
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
|
||||||
|
|
||||||
const renderStars = (rating: number) => {
|
const renderStars = (rating: number) => {
|
||||||
return Array.from({ length: 5 }, (_, i) => (
|
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 (
|
||||||
|
<WBProductCards
|
||||||
|
onBack={() => setSelectedVariant(null)}
|
||||||
|
onComplete={handleCardsComplete}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedVariant === 'wholesaler') {
|
if (selectedVariant === 'wholesaler') {
|
||||||
if (selectedWholesaler) {
|
if (selectedWholesaler) {
|
||||||
return (
|
return (
|
||||||
@ -267,8 +313,8 @@ export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormP
|
|||||||
Создание поставки через выбор товаров по карточкам
|
Создание поставки через выбор товаров по карточкам
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-500/30">
|
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">
|
||||||
В разработке
|
Доступно
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -330,8 +330,13 @@ export function CreateSupplyPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateSupply = () => {
|
const handleCreateSupply = () => {
|
||||||
|
if (selectedVariant === 'cards') {
|
||||||
|
console.log('Создание поставки с карточками Wildberries')
|
||||||
|
// TODO: Здесь будет создание поставки с данными карточек
|
||||||
|
} else {
|
||||||
console.log('Создание поставки с товарами:', selectedProducts)
|
console.log('Создание поставки с товарами:', selectedProducts)
|
||||||
// TODO: Здесь будет реальное создание поставки
|
// TODO: Здесь будет реальное создание поставки
|
||||||
|
}
|
||||||
router.push('/supplies')
|
router.push('/supplies')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1084,11 +1089,11 @@ export function CreateSupplyPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-semibold text-white mb-3">Карточки</h3>
|
<h3 className="text-2xl font-semibold text-white mb-3">Карточки</h3>
|
||||||
<p className="text-white/60">
|
<p className="text-white/60">
|
||||||
Создание поставки через выбор товаров по карточкам
|
Создание поставки через выбор товаров по карточкам Wildberries
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-500/30 text-lg px-4 py-2">
|
<Badge className="bg-green-500/20 text-green-300 border-green-500/30 text-lg px-4 py-2">
|
||||||
В разработке
|
Доступно
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
730
src/components/supplies/wb-product-cards.tsx
Normal file
730
src/components/supplies/wb-product-cards.tsx
Normal file
@ -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<WildberriesCard[]>([])
|
||||||
|
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
|
||||||
|
const [showSummary, setShowSummary] = useState(false)
|
||||||
|
const [fulfillmentServices, setFulfillmentServices] = useState<FulfillmentService[]>([])
|
||||||
|
|
||||||
|
// Загружаем контрагентов-фулфилментов
|
||||||
|
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<string, string>
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowSummary(false)}
|
||||||
|
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Назад к товарам
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-1">Сводка заказа</h2>
|
||||||
|
<p className="text-white/60">Проверьте данные перед созданием поставки</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onBack}
|
||||||
|
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="lg:col-span-2 space-y-4">
|
||||||
|
{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 (
|
||||||
|
<Card key={sc.card.nmID} className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<img
|
||||||
|
src={sc.card.mediaFiles[0] || '/api/placeholder/120/120'}
|
||||||
|
alt={sc.card.title}
|
||||||
|
className="w-20 h-20 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-medium">{sc.card.title}</h3>
|
||||||
|
<p className="text-white/60 text-sm">{sc.card.vendorCode}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Количество:</span>
|
||||||
|
<span className="text-white ml-2">{sc.selectedQuantity}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Рынок:</span>
|
||||||
|
<span className="text-white ml-2">{markets.find(m => m.value === sc.selectedMarket)?.label || 'Не выбран'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Место:</span>
|
||||||
|
<span className="text-white ml-2">{sc.selectedPlace || 'Не указано'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Продавец:</span>
|
||||||
|
<span className="text-white ml-2">{sc.sellerName || 'Не указан'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Телефон:</span>
|
||||||
|
<span className="text-white ml-2">{sc.sellerPhone || 'Не указан'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Дата поставки:</span>
|
||||||
|
<span className="text-white ml-2">{sc.deliveryDate || 'Не выбрана'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sc.selectedServices.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-sm mb-2">Услуги:</p>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{sc.selectedServices.map(serviceId => {
|
||||||
|
const service = fulfillmentServices.find(s => s.id === serviceId)
|
||||||
|
return service ? (
|
||||||
|
<Badge key={serviceId} className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs">
|
||||||
|
{service.name} ({formatCurrency(service.price)})
|
||||||
|
</Badge>
|
||||||
|
) : null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-white font-bold text-lg">{formatCurrency(totalPrice)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-6 sticky top-4">
|
||||||
|
<h3 className="text-white font-semibold text-lg mb-4">Итого</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-white/60">Товаров:</span>
|
||||||
|
<span className="text-white">{getTotalItems()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-white/60">Карточек:</span>
|
||||||
|
<span className="text-white">{selectedCards.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-white/20 pt-3 flex justify-between">
|
||||||
|
<span className="text-white font-semibold">Общая сумма:</span>
|
||||||
|
<span className="text-white font-bold text-xl">{formatCurrency(getTotalAmount())}</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
|
||||||
|
onClick={handleCreateSupply}
|
||||||
|
disabled={creatingSupply}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
{creatingSupply ? 'Создание...' : 'Создать поставку'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onBack}
|
||||||
|
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Назад
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-1">Карточки товаров Wildberries</h2>
|
||||||
|
<p className="text-white/60">Найдите и выберите товары для поставки</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedCards.length > 0 && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowSummary(true)}
|
||||||
|
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||||
|
Корзина ({getTotalItems()})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поиск */}
|
||||||
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
placeholder="Поиск товаров по названию, артикулу или бренду..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="bg-white/5 border-white/20 text-white placeholder-white/50"
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && searchCards()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={searchCards}
|
||||||
|
disabled={loading || !searchTerm.trim()}
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600"
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4 mr-2" />
|
||||||
|
{loading ? 'Поиск...' : 'Найти'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Карточки товаров */}
|
||||||
|
{wbCards.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
{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 key={card.nmID} className={`bg-white/10 backdrop-blur border-white/20 transition-all ${isSelected ? 'ring-2 ring-purple-500/50' : ''}`}>
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* Изображение и основная информация */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<img
|
||||||
|
src={card.mediaFiles[0] || '/api/placeholder/300/200'}
|
||||||
|
alt={card.title}
|
||||||
|
className="w-full h-48 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-medium text-lg mb-1">{card.title}</h3>
|
||||||
|
<p className="text-white/60 text-sm mb-2">{card.vendorCode}</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-white font-bold text-xl">{formatCurrency(price)}</span>
|
||||||
|
<Badge className={`${maxQuantity > 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} шт.
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Количество */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-white text-sm">Количество для заказа</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => updateCardSelection(card, 'selectedQuantity', Math.max(0, selectedQuantity - 1))}
|
||||||
|
disabled={selectedQuantity <= 0}
|
||||||
|
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<Minus className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max={maxQuantity}
|
||||||
|
value={selectedQuantity}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => updateCardSelection(card, 'selectedQuantity', Math.min(maxQuantity, selectedQuantity + 1))}
|
||||||
|
disabled={selectedQuantity >= maxQuantity}
|
||||||
|
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Детальные настройки для выбранных товаров */}
|
||||||
|
{isSelected && selectedCard && (
|
||||||
|
<div className="space-y-4 pt-4 border-t border-white/20">
|
||||||
|
{/* Рынок */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-white text-sm flex items-center">
|
||||||
|
<MapPin className="h-3 w-3 mr-1" />
|
||||||
|
Рынок
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedCard.selectedMarket}
|
||||||
|
onValueChange={(value) => updateCardSelection(card, 'selectedMarket', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-white/5 border-white/20 text-white">
|
||||||
|
<SelectValue placeholder="Выберите рынок" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{markets.map((market) => (
|
||||||
|
<SelectItem key={market.value} value={market.value}>
|
||||||
|
{market.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Место на рынке */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-white text-sm">Место на рынке</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Например: Ряд 5, место 23"
|
||||||
|
value={selectedCard.selectedPlace}
|
||||||
|
onChange={(e) => updateCardSelection(card, 'selectedPlace', e.target.value)}
|
||||||
|
className="bg-white/5 border-white/20 text-white placeholder-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Данные продавца */}
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-white text-sm flex items-center">
|
||||||
|
<User className="h-3 w-3 mr-1" />
|
||||||
|
Имя продавца
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Имя продавца"
|
||||||
|
value={selectedCard.sellerName}
|
||||||
|
onChange={(e) => updateCardSelection(card, 'sellerName', e.target.value)}
|
||||||
|
className="bg-white/5 border-white/20 text-white placeholder-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-white text-sm flex items-center">
|
||||||
|
<Phone className="h-3 w-3 mr-1" />
|
||||||
|
Телефон продавца
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="+7 (999) 123-45-67"
|
||||||
|
value={selectedCard.sellerPhone}
|
||||||
|
onChange={(e) => updateCardSelection(card, 'sellerPhone', e.target.value)}
|
||||||
|
className="bg-white/5 border-white/20 text-white placeholder-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Дата поставки */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-white text-sm flex items-center">
|
||||||
|
<Calendar className="h-3 w-3 mr-1" />
|
||||||
|
Дата поставки
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={selectedCard.deliveryDate}
|
||||||
|
onChange={(e) => updateCardSelection(card, 'deliveryDate', e.target.value)}
|
||||||
|
className="bg-white/5 border-white/20 text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Услуги фулфилмента */}
|
||||||
|
{fulfillmentServices.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-white text-sm flex items-center">
|
||||||
|
<Wrench className="h-3 w-3 mr-1" />
|
||||||
|
Услуги фулфилмента
|
||||||
|
</Label>
|
||||||
|
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||||
|
{fulfillmentServices.map((service) => (
|
||||||
|
<label key={service.id} className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedCard.selectedServices.includes(service.id)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newServices = e.target.checked
|
||||||
|
? [...selectedCard.selectedServices, service.id]
|
||||||
|
: selectedCard.selectedServices.filter(id => id !== service.id)
|
||||||
|
updateCardSelection(card, 'selectedServices', newServices)
|
||||||
|
}}
|
||||||
|
className="rounded border-white/20 bg-white/5 text-purple-500"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-white text-sm">{service.name}</div>
|
||||||
|
<div className="text-white/60 text-xs">
|
||||||
|
{service.organizationName} • {formatCurrency(service.price)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedCards.length > 1 && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => applyServicesToAll(selectedCard.selectedServices)}
|
||||||
|
className="w-full text-xs border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
|
||||||
|
>
|
||||||
|
Применить ко всем карточкам
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Плавающая корзина */}
|
||||||
|
{selectedCards.length > 0 && !showSummary && (
|
||||||
|
<div className="fixed bottom-6 right-6 z-50">
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowSummary(true)}
|
||||||
|
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-lg h-12 px-6"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="h-5 w-5 mr-2" />
|
||||||
|
Корзина ({getTotalItems()}) • {formatCurrency(getTotalAmount())}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{wbCards.length === 0 && !loading && (
|
||||||
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-12">
|
||||||
|
<div className="text-center">
|
||||||
|
<Package className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-2">Поиск товаров</h3>
|
||||||
|
<p className="text-white/60 mb-4">
|
||||||
|
Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1030,6 +1030,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`
|
export const ADMIN_LOGIN = gql`
|
||||||
mutation AdminLogin($username: String!, $password: String!) {
|
mutation AdminLogin($username: String!, $password: String!) {
|
||||||
|
@ -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`
|
export const ADMIN_ME = gql`
|
||||||
query AdminMe {
|
query AdminMe {
|
||||||
|
@ -591,6 +591,8 @@ export const resolvers = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Мои товары (для оптовиков)
|
// Мои товары (для оптовиков)
|
||||||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||||||
if (!context.user) {
|
if (!context.user) {
|
||||||
@ -3598,6 +3600,50 @@ export const resolvers = {
|
|||||||
console.error('Error updating employee schedule:', error)
|
console.error('Error updating employee schedule:', error)
|
||||||
return false
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -34,6 +34,9 @@ export const typeDefs = gql`
|
|||||||
# Логистика организации
|
# Логистика организации
|
||||||
myLogistics: [Logistics!]!
|
myLogistics: [Logistics!]!
|
||||||
|
|
||||||
|
# Поставки Wildberries
|
||||||
|
myWildberriesSupplies: [WildberriesSupply!]!
|
||||||
|
|
||||||
# Товары оптовика
|
# Товары оптовика
|
||||||
myProducts: [Product!]!
|
myProducts: [Product!]!
|
||||||
|
|
||||||
@ -140,6 +143,11 @@ export const typeDefs = gql`
|
|||||||
deleteEmployee(id: ID!): Boolean!
|
deleteEmployee(id: ID!): Boolean!
|
||||||
updateEmployeeSchedule(input: UpdateScheduleInput!): 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!
|
adminLogin(username: String!, password: String!): AdminAuthResponse!
|
||||||
adminLogout: Boolean!
|
adminLogout: Boolean!
|
||||||
@ -693,4 +701,81 @@ export const typeDefs = gql`
|
|||||||
total: Int!
|
total: Int!
|
||||||
hasMore: Boolean!
|
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
|
||||||
|
}
|
||||||
`
|
`
|
@ -11,8 +11,57 @@ interface WildberriesWarehousesResponse {
|
|||||||
data: WildberriesWarehouse[]
|
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 {
|
export class WildberriesService {
|
||||||
private static baseUrl = 'https://marketplace-api.wildberries.ru'
|
private static baseUrl = 'https://marketplace-api.wildberries.ru'
|
||||||
|
private static contentUrl = 'https://content-api.wildberries.ru'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить список складов WB
|
* Получить список складов WB
|
||||||
@ -39,6 +88,56 @@ export class WildberriesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить карточки товаров
|
||||||
|
*/
|
||||||
|
static async getCards(apiKey: string, filter?: WildberriesCardFilter): Promise<WildberriesCard[]> {
|
||||||
|
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<WildberriesCard[]> {
|
||||||
|
const filter: WildberriesCardFilter = {
|
||||||
|
sort: {
|
||||||
|
cursor: {
|
||||||
|
limit
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
textSearch: searchText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getCards(apiKey, filter)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Валидация API ключа WB
|
* Валидация API ключа WB
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user