Compare commits
2 Commits
0e584749f3
...
f71262577a
Author | SHA1 | Date | |
---|---|---|---|
f71262577a | |||
d3fb590c6e |
50
src/app/api/placeholder/[...params]/route.ts
Normal file
50
src/app/api/placeholder/[...params]/route.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ params: string[] }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const resolvedParams = await params
|
||||||
|
const [width, height] = resolvedParams.params[0]?.split('/') || ['400', '400']
|
||||||
|
const searchParams = request.nextUrl.searchParams
|
||||||
|
const text = searchParams.get('text') || 'Image'
|
||||||
|
|
||||||
|
// Создаем простое SVG изображение
|
||||||
|
const svg = `
|
||||||
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#f0f0f0"/>
|
||||||
|
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
||||||
|
font-family="Arial, sans-serif" font-size="16" fill="#666">
|
||||||
|
${text} ${width}x${height}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
return new NextResponse(svg, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/svg+xml',
|
||||||
|
'Cache-Control': 'public, max-age=31536000'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Placeholder API error:', error)
|
||||||
|
|
||||||
|
// Возвращаем простое SVG в случае ошибки
|
||||||
|
const svg = `
|
||||||
|
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#f0f0f0"/>
|
||||||
|
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
||||||
|
font-family="Arial, sans-serif" font-size="16" fill="#666">
|
||||||
|
No Image
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
return new NextResponse(svg, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/svg+xml'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1447,7 +1447,7 @@ export function NavigationDemo() {
|
|||||||
|
|
||||||
{/* Load More Pattern */}
|
{/* Load More Pattern */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-white/90 text-sm font-medium mb-3">Паттерн "Загрузить еще"</h4>
|
<h4 className="text-white/90 text-sm font-medium mb-3">Паттерн "Загрузить еще"</h4>
|
||||||
<div className="glass-card p-4 rounded-xl border border-white/10 text-center">
|
<div className="glass-card p-4 rounded-xl border border-white/10 text-center">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-white/70 text-sm">
|
<div className="text-white/70 text-sm">
|
||||||
|
@ -526,7 +526,7 @@ export function MaterialsOrderForm() {
|
|||||||
: "Партнеры не найдены"}
|
: "Партнеры не найдены"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-white/40 text-sm mt-2">
|
<p className="text-white/40 text-sm mt-2">
|
||||||
Добавьте партнеров в разделе "Партнеры"
|
Добавьте партнеров в разделе "Партнеры"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { WBProductCards } from './wb-product-cards'
|
||||||
|
|
||||||
interface WholesalerForCreation {
|
interface WholesalerForCreation {
|
||||||
id: string
|
id: string
|
||||||
@ -72,6 +73,34 @@ interface SelectedProduct extends WholesalerProduct {
|
|||||||
wholesalerName: string
|
wholesalerName: 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[]
|
||||||
|
}
|
||||||
|
|
||||||
// Моковые данные оптовиков
|
// Моковые данные оптовиков
|
||||||
const mockWholesalers: WholesalerForCreation[] = [
|
const mockWholesalers: WholesalerForCreation[] = [
|
||||||
{
|
{
|
||||||
@ -229,6 +258,7 @@ export function CreateSupplyPage() {
|
|||||||
const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null)
|
const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null)
|
||||||
const [selectedWholesaler, setSelectedWholesaler] = useState<WholesalerForCreation | null>(null)
|
const [selectedWholesaler, setSelectedWholesaler] = useState<WholesalerForCreation | null>(null)
|
||||||
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>([])
|
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>([])
|
||||||
|
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
|
||||||
const [showSummary, setShowSummary] = useState(false)
|
const [showSummary, setShowSummary] = useState(false)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
@ -329,6 +359,13 @@ export function CreateSupplyPage() {
|
|||||||
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
|
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCardsComplete = (cards: SelectedCard[]) => {
|
||||||
|
setSelectedCards(cards)
|
||||||
|
console.log('Карточки товаров выбраны:', cards)
|
||||||
|
// TODO: Здесь будет создание поставки с данными карточек
|
||||||
|
router.push('/supplies')
|
||||||
|
}
|
||||||
|
|
||||||
const handleCreateSupply = () => {
|
const handleCreateSupply = () => {
|
||||||
if (selectedVariant === 'cards') {
|
if (selectedVariant === 'cards') {
|
||||||
console.log('Создание поставки с карточками Wildberries')
|
console.log('Создание поставки с карточками Wildberries')
|
||||||
@ -706,6 +743,16 @@ export function CreateSupplyPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Рендер карточек Wildberries
|
||||||
|
if (selectedVariant === 'cards') {
|
||||||
|
return (
|
||||||
|
<WBProductCards
|
||||||
|
onBack={() => setSelectedVariant(null)}
|
||||||
|
onComplete={handleCardsComplete}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Рендер выбора оптовиков
|
// Рендер выбора оптовиков
|
||||||
if (selectedVariant === 'wholesaler') {
|
if (selectedVariant === 'wholesaler') {
|
||||||
return (
|
return (
|
||||||
|
@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Plus,
|
Plus,
|
||||||
@ -20,7 +21,9 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Check,
|
Check,
|
||||||
X
|
Eye,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { WildberriesService } from '@/services/wildberries-service'
|
import { WildberriesService } from '@/services/wildberries-service'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
@ -82,6 +85,176 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
|
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
|
||||||
const [showSummary, setShowSummary] = useState(false)
|
const [showSummary, setShowSummary] = useState(false)
|
||||||
const [fulfillmentServices, setFulfillmentServices] = useState<FulfillmentService[]>([])
|
const [fulfillmentServices, setFulfillmentServices] = useState<FulfillmentService[]>([])
|
||||||
|
const [selectedCardForDetails, setSelectedCardForDetails] = useState<WildberriesCard | null>(null)
|
||||||
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
|
|
||||||
|
// Моковые товары для демонстрации
|
||||||
|
const getMockCards = (): WildberriesCard[] => [
|
||||||
|
{
|
||||||
|
nmID: 123456789,
|
||||||
|
vendorCode: 'SKU001',
|
||||||
|
title: 'Смартфон Samsung Galaxy A54',
|
||||||
|
description: 'Современный смартфон с отличной камерой и долгим временем автономной работы',
|
||||||
|
brand: 'Samsung',
|
||||||
|
object: 'Смартфоны',
|
||||||
|
parent: 'Электроника',
|
||||||
|
countryProduction: 'Корея',
|
||||||
|
supplierVendorCode: 'SUPPLIER-001',
|
||||||
|
mediaFiles: ['/api/placeholder/400/400', '/api/placeholder/400/401', '/api/placeholder/400/402'],
|
||||||
|
sizes: [
|
||||||
|
{
|
||||||
|
chrtID: 123456,
|
||||||
|
techSize: '128GB',
|
||||||
|
wbSize: '128GB Черный',
|
||||||
|
price: 25990,
|
||||||
|
discountedPrice: 22990,
|
||||||
|
quantity: 15
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nmID: 987654321,
|
||||||
|
vendorCode: 'SKU002',
|
||||||
|
title: 'Наушники Apple AirPods Pro',
|
||||||
|
description: 'Беспроводные наушники с активным шумоподавлением и пространственным звуком',
|
||||||
|
brand: 'Apple',
|
||||||
|
object: 'Наушники',
|
||||||
|
parent: 'Электроника',
|
||||||
|
countryProduction: 'Китай',
|
||||||
|
supplierVendorCode: 'SUPPLIER-002',
|
||||||
|
mediaFiles: ['/api/placeholder/400/403', '/api/placeholder/400/404'],
|
||||||
|
sizes: [
|
||||||
|
{
|
||||||
|
chrtID: 987654,
|
||||||
|
techSize: 'Standard',
|
||||||
|
wbSize: 'Белый',
|
||||||
|
price: 24990,
|
||||||
|
discountedPrice: 19990,
|
||||||
|
quantity: 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nmID: 555666777,
|
||||||
|
vendorCode: 'SKU003',
|
||||||
|
title: 'Кроссовки Nike Air Max 270',
|
||||||
|
description: 'Спортивные кроссовки с современным дизайном и комфортной посадкой',
|
||||||
|
brand: 'Nike',
|
||||||
|
object: 'Кроссовки',
|
||||||
|
parent: 'Обувь',
|
||||||
|
countryProduction: 'Вьетнам',
|
||||||
|
supplierVendorCode: 'SUPPLIER-003',
|
||||||
|
mediaFiles: ['/api/placeholder/400/405', '/api/placeholder/400/406', '/api/placeholder/400/407'],
|
||||||
|
sizes: [
|
||||||
|
{
|
||||||
|
chrtID: 555666,
|
||||||
|
techSize: '42',
|
||||||
|
wbSize: '42 EU',
|
||||||
|
price: 12990,
|
||||||
|
discountedPrice: 9990,
|
||||||
|
quantity: 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chrtID: 555667,
|
||||||
|
techSize: '43',
|
||||||
|
wbSize: '43 EU',
|
||||||
|
price: 12990,
|
||||||
|
discountedPrice: 9990,
|
||||||
|
quantity: 20
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nmID: 444333222,
|
||||||
|
vendorCode: 'SKU004',
|
||||||
|
title: 'Футболка Adidas Originals',
|
||||||
|
description: 'Классическая футболка из органического хлопка с логотипом бренда',
|
||||||
|
brand: 'Adidas',
|
||||||
|
object: 'Футболки',
|
||||||
|
parent: 'Одежда',
|
||||||
|
countryProduction: 'Бангладеш',
|
||||||
|
supplierVendorCode: 'SUPPLIER-004',
|
||||||
|
mediaFiles: ['/api/placeholder/400/408', '/api/placeholder/400/409'],
|
||||||
|
sizes: [
|
||||||
|
{
|
||||||
|
chrtID: 444333,
|
||||||
|
techSize: 'M',
|
||||||
|
wbSize: 'M',
|
||||||
|
price: 2990,
|
||||||
|
discountedPrice: 2490,
|
||||||
|
quantity: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chrtID: 444334,
|
||||||
|
techSize: 'L',
|
||||||
|
wbSize: 'L',
|
||||||
|
price: 2990,
|
||||||
|
discountedPrice: 2490,
|
||||||
|
quantity: 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chrtID: 444335,
|
||||||
|
techSize: 'XL',
|
||||||
|
wbSize: 'XL',
|
||||||
|
price: 2990,
|
||||||
|
discountedPrice: 2490,
|
||||||
|
quantity: 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nmID: 111222333,
|
||||||
|
vendorCode: 'SKU005',
|
||||||
|
title: 'Рюкзак для ноутбука Xiaomi',
|
||||||
|
description: 'Стильный и функциональный рюкзак для ноутбука до 15.6 дюймов',
|
||||||
|
brand: 'Xiaomi',
|
||||||
|
object: 'Рюкзаки',
|
||||||
|
parent: 'Аксессуары',
|
||||||
|
countryProduction: 'Китай',
|
||||||
|
supplierVendorCode: 'SUPPLIER-005',
|
||||||
|
mediaFiles: ['/api/placeholder/400/410'],
|
||||||
|
sizes: [
|
||||||
|
{
|
||||||
|
chrtID: 111222,
|
||||||
|
techSize: '15.6"',
|
||||||
|
wbSize: 'Черный',
|
||||||
|
price: 4990,
|
||||||
|
discountedPrice: 3990,
|
||||||
|
quantity: 35
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nmID: 777888999,
|
||||||
|
vendorCode: 'SKU006',
|
||||||
|
title: 'Умные часы Apple Watch Series 9',
|
||||||
|
description: 'Новейшие умные часы с передовыми функциями здоровья и фитнеса',
|
||||||
|
brand: 'Apple',
|
||||||
|
object: 'Умные часы',
|
||||||
|
parent: 'Электроника',
|
||||||
|
countryProduction: 'Китай',
|
||||||
|
supplierVendorCode: 'SUPPLIER-006',
|
||||||
|
mediaFiles: ['/api/placeholder/400/411', '/api/placeholder/400/412', '/api/placeholder/400/413'],
|
||||||
|
sizes: [
|
||||||
|
{
|
||||||
|
chrtID: 777888,
|
||||||
|
techSize: '41mm',
|
||||||
|
wbSize: '41mm GPS',
|
||||||
|
price: 39990,
|
||||||
|
discountedPrice: 35990,
|
||||||
|
quantity: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chrtID: 777889,
|
||||||
|
techSize: '45mm',
|
||||||
|
wbSize: '45mm GPS',
|
||||||
|
price: 42990,
|
||||||
|
discountedPrice: 38990,
|
||||||
|
quantity: 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// Загружаем контрагентов-фулфилментов
|
// Загружаем контрагентов-фулфилментов
|
||||||
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES)
|
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES)
|
||||||
@ -113,12 +286,19 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Загружаем услуги фулфилмента из контрагентов
|
// Загружаем услуги фулфилмента из контрагентов
|
||||||
if (counterpartiesData?.myCounterparties) {
|
if (counterpartiesData?.myCounterparties) {
|
||||||
|
interface Organization {
|
||||||
|
id: string
|
||||||
|
name?: string
|
||||||
|
fullName?: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
const fulfillmentOrganizations = counterpartiesData.myCounterparties.filter(
|
const fulfillmentOrganizations = counterpartiesData.myCounterparties.filter(
|
||||||
(org: any) => org.type === 'FULFILLMENT'
|
(org: Organization) => org.type === 'FULFILLMENT'
|
||||||
)
|
)
|
||||||
|
|
||||||
// В реальном приложении здесь был бы запрос услуг для каждой организации
|
// В реальном приложении здесь был бы запрос услуг для каждой организации
|
||||||
const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: any) => [
|
const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: Organization) => [
|
||||||
{
|
{
|
||||||
id: `${org.id}-packaging`,
|
id: `${org.id}-packaging`,
|
||||||
name: 'Упаковка товаров',
|
name: 'Упаковка товаров',
|
||||||
@ -146,83 +326,155 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
}
|
}
|
||||||
}, [counterpartiesData])
|
}, [counterpartiesData])
|
||||||
|
|
||||||
const searchCards = async () => {
|
// Автоматически загружаем товары при открытии компонента
|
||||||
if (!searchTerm.trim()) return
|
useEffect(() => {
|
||||||
|
const loadCards = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
||||||
|
|
||||||
|
console.log('WB API Key found:', !!wbApiKey)
|
||||||
|
console.log('WB API Key active:', wbApiKey?.isActive)
|
||||||
|
console.log('WB API Key validationData:', wbApiKey?.validationData)
|
||||||
|
|
||||||
|
if (wbApiKey?.isActive) {
|
||||||
|
// Попытка загрузить реальные данные из API Wildberries
|
||||||
|
const validationData = wbApiKey.validationData as Record<string, string>
|
||||||
|
|
||||||
|
// API ключ может храниться в разных местах
|
||||||
|
const apiToken = validationData?.token ||
|
||||||
|
validationData?.apiKey ||
|
||||||
|
validationData?.key ||
|
||||||
|
(wbApiKey as { apiKey?: string }).apiKey // Прямое поле apiKey из базы
|
||||||
|
|
||||||
|
console.log('API Token extracted:', !!apiToken)
|
||||||
|
console.log('API Token length:', apiToken?.length)
|
||||||
|
|
||||||
|
if (apiToken) {
|
||||||
|
console.log('Загружаем карточки из WB API...')
|
||||||
|
const cards = await WildberriesService.getAllCards(apiToken, 50)
|
||||||
|
setWbCards(cards)
|
||||||
|
console.log('Загружено карточек из WB API:', cards.length)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если API ключ не настроен, оставляем пустое состояние
|
||||||
|
console.log('API ключ WB не настроен, показываем пустое состояние')
|
||||||
|
setWbCards([])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки карточек WB:', error)
|
||||||
|
// При ошибке API показываем пустое состояние
|
||||||
|
setWbCards([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCards()
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
const loadAllCards = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
||||||
if (!wbApiKey?.isActive) {
|
|
||||||
throw new Error('API ключ Wildberries не настроен')
|
if (wbApiKey?.isActive) {
|
||||||
}
|
// Попытка загрузить реальные данные из API Wildberries
|
||||||
|
const validationData = wbApiKey.validationData as Record<string, string>
|
||||||
const validationData = wbApiKey.validationData as Record<string, string>
|
const apiToken = validationData?.token ||
|
||||||
const apiToken = validationData?.token || validationData?.apiKey
|
validationData?.apiKey ||
|
||||||
if (!apiToken) {
|
validationData?.key ||
|
||||||
throw new Error('API токен не найден')
|
(wbApiKey as { apiKey?: string }).apiKey
|
||||||
}
|
|
||||||
|
if (apiToken) {
|
||||||
const cards = await WildberriesService.searchCards(apiToken, searchTerm)
|
console.log('Загружаем все карточки из WB API...')
|
||||||
setWbCards(cards)
|
const cards = await WildberriesService.getAllCards(apiToken, 100)
|
||||||
} catch (error) {
|
setWbCards(cards)
|
||||||
console.error('Ошибка поиска карточек:', error)
|
console.log('Загружено карточек из WB API:', cards.length)
|
||||||
// Для демо загрузим моковые данные
|
return
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
])
|
}
|
||||||
|
|
||||||
|
// Если API ключ не настроен, загружаем моковые данные
|
||||||
|
console.log('API ключ WB не настроен, загружаем моковые данные')
|
||||||
|
const allCards = getMockCards()
|
||||||
|
setWbCards(allCards)
|
||||||
|
console.log('Загружены моковые товары:', allCards.length)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки всех карточек WB:', error)
|
||||||
|
// При ошибке загружаем моковые данные
|
||||||
|
const allCards = getMockCards()
|
||||||
|
setWbCards(allCards)
|
||||||
|
console.log('Загружены моковые товары (fallback):', allCards.length)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: any) => {
|
const searchCards = async () => {
|
||||||
|
if (!searchTerm.trim()) {
|
||||||
|
loadAllCards()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
||||||
|
|
||||||
|
if (wbApiKey?.isActive) {
|
||||||
|
// Попытка поиска в реальном API Wildberries
|
||||||
|
const validationData = wbApiKey.validationData as Record<string, string>
|
||||||
|
const apiToken = validationData?.token ||
|
||||||
|
validationData?.apiKey ||
|
||||||
|
validationData?.key ||
|
||||||
|
(wbApiKey as { apiKey?: string }).apiKey
|
||||||
|
|
||||||
|
if (apiToken) {
|
||||||
|
console.log('Поиск в WB API:', searchTerm)
|
||||||
|
const cards = await WildberriesService.searchCards(apiToken, searchTerm, 50)
|
||||||
|
setWbCards(cards)
|
||||||
|
console.log('Найдено карточек в WB API:', cards.length)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если API ключ не настроен, ищем в моковых данных
|
||||||
|
console.log('API ключ WB не настроен, поиск в моковых данных:', searchTerm)
|
||||||
|
const mockCards = getMockCards()
|
||||||
|
|
||||||
|
// Фильтруем товары по поисковому запросу
|
||||||
|
const filteredCards = mockCards.filter(card =>
|
||||||
|
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
card.object.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
setWbCards(filteredCards)
|
||||||
|
console.log('Найдено моковых товаров:', filteredCards.length)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка поиска карточек WB:', error)
|
||||||
|
// При ошибке ищем в моковых данных
|
||||||
|
const mockCards = getMockCards()
|
||||||
|
const filteredCards = mockCards.filter(card =>
|
||||||
|
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
card.object.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
setWbCards(filteredCards)
|
||||||
|
console.log('Найдено моковых товаров (fallback):', filteredCards.length)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: string | number | string[]) => {
|
||||||
setSelectedCards(prev => {
|
setSelectedCards(prev => {
|
||||||
const existing = prev.find(sc => sc.card.nmID === card.nmID)
|
const existing = prev.find(sc => sc.card.nmID === card.nmID)
|
||||||
|
|
||||||
if (field === 'selectedQuantity' && value === 0) {
|
if (field === 'selectedQuantity' && typeof value === 'number' && value === 0) {
|
||||||
return prev.filter(sc => sc.card.nmID !== card.nmID)
|
return prev.filter(sc => sc.card.nmID !== card.nmID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,10 +484,10 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
? { ...sc, [field]: value }
|
? { ...sc, [field]: value }
|
||||||
: sc
|
: sc
|
||||||
)
|
)
|
||||||
} else if (field === 'selectedQuantity' && value > 0) {
|
} else if (field === 'selectedQuantity' && typeof value === 'number' && value > 0) {
|
||||||
const newSelectedCard: SelectedCard = {
|
const newSelectedCard: SelectedCard = {
|
||||||
card,
|
card,
|
||||||
selectedQuantity: value,
|
selectedQuantity: value as number,
|
||||||
selectedMarket: '',
|
selectedMarket: '',
|
||||||
selectedPlace: '',
|
selectedPlace: '',
|
||||||
sellerName: '',
|
sellerName: '',
|
||||||
@ -284,6 +536,28 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCardClick = (card: WildberriesCard) => {
|
||||||
|
setSelectedCardForDetails(card)
|
||||||
|
setCurrentImageIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDetailsModal = () => {
|
||||||
|
setSelectedCardForDetails(null)
|
||||||
|
setCurrentImageIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextImage = () => {
|
||||||
|
if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) {
|
||||||
|
setCurrentImageIndex((prev) => (prev + 1) % selectedCardForDetails.mediaFiles.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevImage = () => {
|
||||||
|
if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) {
|
||||||
|
setCurrentImageIndex((prev) => (prev - 1 + selectedCardForDetails.mediaFiles.length) % selectedCardForDetails.mediaFiles.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleCreateSupply = async () => {
|
const handleCreateSupply = async () => {
|
||||||
try {
|
try {
|
||||||
const supplyInput = {
|
const supplyInput = {
|
||||||
@ -356,7 +630,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
<Card key={sc.card.nmID} className="bg-white/10 backdrop-blur border-white/20 p-4">
|
<Card key={sc.card.nmID} className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<img
|
<img
|
||||||
src={sc.card.mediaFiles[0] || '/api/placeholder/120/120'}
|
src={sc.card.mediaFiles?.[0] || '/api/placeholder/120/120'}
|
||||||
alt={sc.card.title}
|
alt={sc.card.title}
|
||||||
className="w-20 h-20 rounded-lg object-cover"
|
className="w-20 h-20 rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
@ -504,8 +778,26 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Состояние загрузки */}
|
||||||
|
{loading && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<Card key={i} className="bg-white/10 backdrop-blur border-white/20 p-4 animate-pulse">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white/20 rounded-lg h-48 w-full"></div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="bg-white/20 rounded h-4 w-3/4"></div>
|
||||||
|
<div className="bg-white/20 rounded h-4 w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/20 rounded h-10 w-full"></div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Карточки товаров */}
|
{/* Карточки товаров */}
|
||||||
{wbCards.length > 0 && (
|
{!loading && wbCards.length > 0 && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
{wbCards.map((card) => {
|
{wbCards.map((card) => {
|
||||||
const selectedQuantity = getSelectedQuantity(card)
|
const selectedQuantity = getSelectedQuantity(card)
|
||||||
@ -520,13 +812,28 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{/* Изображение и основная информация */}
|
{/* Изображение и основная информация */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<img
|
<div className="relative group">
|
||||||
src={card.mediaFiles[0] || '/api/placeholder/300/200'}
|
<img
|
||||||
alt={card.title}
|
src={card.mediaFiles?.[0] || '/api/placeholder/300/200'}
|
||||||
className="w-full h-48 rounded-lg object-cover"
|
alt={card.title}
|
||||||
/>
|
className="w-full h-48 rounded-lg object-cover cursor-pointer"
|
||||||
|
onClick={() => handleCardClick(card)}
|
||||||
|
/>
|
||||||
|
{/* Кнопка "Подробнее" при наведении */}
|
||||||
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center rounded-lg">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleCardClick(card)}
|
||||||
|
className="bg-white/20 backdrop-blur text-white border-white/30 hover:bg-white/30"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4 mr-2" />
|
||||||
|
Подробнее
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-medium text-lg mb-1">{card.title}</h3>
|
<h3 className="text-white font-medium text-lg mb-1 cursor-pointer hover:text-purple-300 transition-colors" onClick={() => handleCardClick(card)}>{card.title}</h3>
|
||||||
<p className="text-white/60 text-sm mb-2">{card.vendorCode}</p>
|
<p className="text-white/60 text-sm mb-2">{card.vendorCode}</p>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-white font-bold text-xl">{formatCurrency(price)}</span>
|
<span className="text-white font-bold text-xl">{formatCurrency(price)}</span>
|
||||||
@ -718,13 +1025,183 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
|||||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-12">
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-12">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Package className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
<Package className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-semibold text-white mb-2">Поиск товаров</h3>
|
<h3 className="text-xl font-semibold text-white mb-2">Карточки товаров Wildberries</h3>
|
||||||
<p className="text-white/60 mb-4">
|
{user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive ? (
|
||||||
Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries
|
<>
|
||||||
</p>
|
<p className="text-white/60 mb-6">
|
||||||
|
Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries, или загрузите все доступные карточки
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={loadAllCards}
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white"
|
||||||
|
>
|
||||||
|
<Package className="h-4 w-4 mr-2" />
|
||||||
|
Загрузить карточки из WB API
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-white/60 mb-4">
|
||||||
|
Для работы с реальными карточками товаров необходимо настроить API ключ Wildberries в настройках организации
|
||||||
|
</p>
|
||||||
|
<p className="text-white/40 text-sm mb-6">
|
||||||
|
Сейчас показаны демонстрационные товары. Для тестирования используйте поиск или загрузите все.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={loadAllCards}
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white"
|
||||||
|
>
|
||||||
|
<Package className="h-4 w-4 mr-2" />
|
||||||
|
Показать демо товары
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно с детальной информацией о товаре */}
|
||||||
|
<Dialog open={!!selectedCardForDetails} onOpenChange={(open) => !open && closeDetailsModal()}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] bg-black/90 backdrop-blur-xl border border-white/20 p-0">
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>Детальная информация о товаре</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{selectedCardForDetails && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
|
||||||
|
{/* Изображения */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={selectedCardForDetails.mediaFiles?.[currentImageIndex] || '/api/placeholder/400/400'}
|
||||||
|
alt={selectedCardForDetails.title}
|
||||||
|
className="w-full aspect-square rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Навигация по изображениям */}
|
||||||
|
{selectedCardForDetails.mediaFiles?.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={prevImage}
|
||||||
|
className="absolute left-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextImage}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-black/50 px-3 py-1 rounded-full text-white text-sm">
|
||||||
|
{currentImageIndex + 1} из {selectedCardForDetails.mediaFiles?.length || 0}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Миниатюры изображений */}
|
||||||
|
{selectedCardForDetails.mediaFiles?.length > 1 && (
|
||||||
|
<div className="flex space-x-2 overflow-x-auto">
|
||||||
|
{selectedCardForDetails.mediaFiles?.map((image, index) => (
|
||||||
|
<img
|
||||||
|
key={index}
|
||||||
|
src={image}
|
||||||
|
alt={`${selectedCardForDetails.title} ${index + 1}`}
|
||||||
|
className={`w-16 h-16 rounded-lg object-cover cursor-pointer flex-shrink-0 ${
|
||||||
|
index === currentImageIndex ? 'ring-2 ring-purple-500' : 'opacity-60 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => setCurrentImageIndex(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Информация о товаре */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-2">{selectedCardForDetails.title}</h2>
|
||||||
|
<p className="text-white/60 mb-4">Артикул: {selectedCardForDetails.vendorCode}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Бренд:</span>
|
||||||
|
<span className="text-white ml-2">{selectedCardForDetails.brand}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Категория:</span>
|
||||||
|
<span className="text-white ml-2">{selectedCardForDetails.object}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Родительская категория:</span>
|
||||||
|
<span className="text-white ml-2">{selectedCardForDetails.parent}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/60">Страна производства:</span>
|
||||||
|
<span className="text-white ml-2">{selectedCardForDetails.countryProduction}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedCardForDetails.description && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-semibold mb-2">Описание</h3>
|
||||||
|
<p className="text-white/80 text-sm leading-relaxed">{selectedCardForDetails.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Размеры и цены */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-semibold mb-3">Доступные варианты</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedCardForDetails.sizes.map((size) => (
|
||||||
|
<div key={size.chrtID} className="bg-white/5 rounded-lg p-3">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-white font-medium">{size.wbSize}</span>
|
||||||
|
<Badge className={`${size.quantity > 10 ? 'bg-green-500/20 text-green-300' : size.quantity > 0 ? 'bg-yellow-500/20 text-yellow-300' : 'bg-red-500/20 text-red-300'}`}>
|
||||||
|
{size.quantity} шт.
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<span className="text-white/60">Размер: {size.techSize}</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-white font-bold">{formatCurrency(size.discountedPrice || size.price)}</div>
|
||||||
|
{size.discountedPrice && size.discountedPrice < size.price && (
|
||||||
|
<div className="text-white/40 line-through text-xs">{formatCurrency(size.price)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопки действий в модальном окне */}
|
||||||
|
<div className="flex gap-3 pt-4 border-t border-white/20">
|
||||||
|
<Button
|
||||||
|
className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
const currentQuantity = getSelectedQuantity(selectedCardForDetails)
|
||||||
|
updateCardSelection(selectedCardForDetails, 'selectedQuantity', currentQuantity + 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Добавить в корзину
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-white/20 text-white hover:bg-white/10"
|
||||||
|
onClick={closeDetailsModal}
|
||||||
|
>
|
||||||
|
Закрыть
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -177,6 +177,7 @@ export const ADD_MARKETPLACE_API_KEY = gql`
|
|||||||
apiKey {
|
apiKey {
|
||||||
id
|
id
|
||||||
marketplace
|
marketplace
|
||||||
|
apiKey
|
||||||
isActive
|
isActive
|
||||||
validationData
|
validationData
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,11 @@ export const GET_ME = gql`
|
|||||||
apiKeys {
|
apiKeys {
|
||||||
id
|
id
|
||||||
marketplace
|
marketplace
|
||||||
|
apiKey
|
||||||
isActive
|
isActive
|
||||||
validationData
|
validationData
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,6 +256,7 @@ export const GET_ORGANIZATION = gql`
|
|||||||
apiKeys {
|
apiKeys {
|
||||||
id
|
id
|
||||||
marketplace
|
marketplace
|
||||||
|
apiKey
|
||||||
isActive
|
isActive
|
||||||
validationData
|
validationData
|
||||||
createdAt
|
createdAt
|
||||||
|
@ -96,7 +96,7 @@ const generateToken = (payload: AuthTokenPayload): string => {
|
|||||||
const verifyToken = (token: string): AuthTokenPayload => {
|
const verifyToken = (token: string): AuthTokenPayload => {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
|
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new GraphQLError("Недействительный токен", {
|
throw new GraphQLError("Недействительный токен", {
|
||||||
extensions: { code: "UNAUTHENTICATED" },
|
extensions: { code: "UNAUTHENTICATED" },
|
||||||
@ -168,7 +168,7 @@ function parseLiteral(ast: unknown): unknown {
|
|||||||
fields?: unknown[];
|
fields?: unknown[];
|
||||||
values?: unknown[];
|
values?: unknown[];
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (astNode.kind) {
|
switch (astNode.kind) {
|
||||||
case Kind.STRING:
|
case Kind.STRING:
|
||||||
case Kind.BOOLEAN:
|
case Kind.BOOLEAN:
|
||||||
@ -290,7 +290,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
|
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
|
||||||
const outgoingRequests = await prisma.counterpartyRequest.findMany({
|
const outgoingRequests = await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
senderId: currentUser.organization.id,
|
senderId: currentUser.organization.id,
|
||||||
status: "PENDING",
|
status: "PENDING",
|
||||||
},
|
},
|
||||||
@ -301,7 +301,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
// Получаем входящие заявки для добавления флага hasIncomingRequest
|
// Получаем входящие заявки для добавления флага hasIncomingRequest
|
||||||
const incomingRequests = await prisma.counterpartyRequest.findMany({
|
const incomingRequests = await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
receiverId: currentUser.organization.id,
|
receiverId: currentUser.organization.id,
|
||||||
status: "PENDING",
|
status: "PENDING",
|
||||||
},
|
},
|
||||||
@ -365,7 +365,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
const counterparties = await prisma.counterparty.findMany({
|
const counterparties = await prisma.counterparty.findMany({
|
||||||
where: { organizationId: currentUser.organization.id },
|
where: { organizationId: currentUser.organization.id },
|
||||||
include: {
|
include: {
|
||||||
counterparty: {
|
counterparty: {
|
||||||
include: {
|
include: {
|
||||||
users: true,
|
users: true,
|
||||||
@ -396,7 +396,7 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.counterpartyRequest.findMany({
|
return await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
receiverId: currentUser.organization.id,
|
receiverId: currentUser.organization.id,
|
||||||
status: "PENDING",
|
status: "PENDING",
|
||||||
},
|
},
|
||||||
@ -436,7 +436,7 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.counterpartyRequest.findMany({
|
return await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
senderId: currentUser.organization.id,
|
senderId: currentUser.organization.id,
|
||||||
status: { in: ["PENDING", "REJECTED"] },
|
status: { in: ["PENDING", "REJECTED"] },
|
||||||
},
|
},
|
||||||
@ -505,7 +505,7 @@ export const resolvers = {
|
|||||||
receiverOrganization: {
|
receiverOrganization: {
|
||||||
include: {
|
include: {
|
||||||
users: true,
|
users: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
@ -670,7 +670,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
return await prisma.product.findMany({
|
return await prisma.product.findMany({
|
||||||
where: { organizationId: currentUser.organization.id },
|
where: { organizationId: currentUser.organization.id },
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
@ -712,7 +712,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
return await prisma.product.findMany({
|
return await prisma.product.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
@ -897,7 +897,7 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const employee = await prisma.employee.findFirst({
|
const employee = await prisma.employee.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: args.id,
|
id: args.id,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
@ -984,7 +984,7 @@ export const resolvers = {
|
|||||||
args.phone,
|
args.phone,
|
||||||
args.code
|
args.code
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!verificationResult.success) {
|
if (!verificationResult.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -1043,7 +1043,7 @@ export const resolvers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log("verifySmsCode - Returning result:", {
|
console.log("verifySmsCode - Returning result:", {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
hasToken: !!result.token,
|
hasToken: !!result.token,
|
||||||
hasUser: !!result.user,
|
hasUser: !!result.user,
|
||||||
message: result.message,
|
message: result.message,
|
||||||
@ -1147,28 +1147,28 @@ export const resolvers = {
|
|||||||
addressFull: organizationData.addressFull,
|
addressFull: organizationData.addressFull,
|
||||||
ogrn: organizationData.ogrn,
|
ogrn: organizationData.ogrn,
|
||||||
ogrnDate: organizationData.ogrnDate,
|
ogrnDate: organizationData.ogrnDate,
|
||||||
|
|
||||||
// Статус организации
|
// Статус организации
|
||||||
status: organizationData.status,
|
status: organizationData.status,
|
||||||
actualityDate: organizationData.actualityDate,
|
actualityDate: organizationData.actualityDate,
|
||||||
registrationDate: organizationData.registrationDate,
|
registrationDate: organizationData.registrationDate,
|
||||||
liquidationDate: organizationData.liquidationDate,
|
liquidationDate: organizationData.liquidationDate,
|
||||||
|
|
||||||
// Руководитель
|
// Руководитель
|
||||||
managementName: organizationData.managementName,
|
managementName: organizationData.managementName,
|
||||||
managementPost: organizationData.managementPost,
|
managementPost: organizationData.managementPost,
|
||||||
|
|
||||||
// ОПФ
|
// ОПФ
|
||||||
opfCode: organizationData.opfCode,
|
opfCode: organizationData.opfCode,
|
||||||
opfFull: organizationData.opfFull,
|
opfFull: organizationData.opfFull,
|
||||||
opfShort: organizationData.opfShort,
|
opfShort: organizationData.opfShort,
|
||||||
|
|
||||||
// Коды статистики
|
// Коды статистики
|
||||||
okato: organizationData.okato,
|
okato: organizationData.okato,
|
||||||
oktmo: organizationData.oktmo,
|
oktmo: organizationData.oktmo,
|
||||||
okpo: organizationData.okpo,
|
okpo: organizationData.okpo,
|
||||||
okved: organizationData.okved,
|
okved: organizationData.okved,
|
||||||
|
|
||||||
// Контакты
|
// Контакты
|
||||||
phones: organizationData.phones
|
phones: organizationData.phones
|
||||||
? JSON.parse(JSON.stringify(organizationData.phones))
|
? JSON.parse(JSON.stringify(organizationData.phones))
|
||||||
@ -1176,12 +1176,12 @@ export const resolvers = {
|
|||||||
emails: organizationData.emails
|
emails: organizationData.emails
|
||||||
? JSON.parse(JSON.stringify(organizationData.emails))
|
? JSON.parse(JSON.stringify(organizationData.emails))
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
// Финансовые данные
|
// Финансовые данные
|
||||||
employeeCount: organizationData.employeeCount,
|
employeeCount: organizationData.employeeCount,
|
||||||
revenue: organizationData.revenue,
|
revenue: organizationData.revenue,
|
||||||
taxSystem: organizationData.taxSystem,
|
taxSystem: organizationData.taxSystem,
|
||||||
|
|
||||||
type: type,
|
type: type,
|
||||||
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
|
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
|
||||||
},
|
},
|
||||||
@ -1284,7 +1284,7 @@ export const resolvers = {
|
|||||||
const tradeMark = validationResults[0]?.data?.tradeMark;
|
const tradeMark = validationResults[0]?.data?.tradeMark;
|
||||||
const sellerName = validationResults[0]?.data?.sellerName;
|
const sellerName = validationResults[0]?.data?.sellerName;
|
||||||
const shopName = tradeMark || sellerName || "Магазин";
|
const shopName = tradeMark || sellerName || "Магазин";
|
||||||
|
|
||||||
const organization = await prisma.organization.create({
|
const organization = await prisma.organization.create({
|
||||||
data: {
|
data: {
|
||||||
inn:
|
inn:
|
||||||
@ -1427,7 +1427,7 @@ export const resolvers = {
|
|||||||
where: { id: existingKey.id },
|
where: { id: existingKey.id },
|
||||||
data: {
|
data: {
|
||||||
apiKey,
|
apiKey,
|
||||||
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -1453,7 +1453,7 @@ export const resolvers = {
|
|||||||
message: "API ключ успешно добавлен",
|
message: "API ключ успешно добавлен",
|
||||||
apiKey: newKey,
|
apiKey: newKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding marketplace API key:", error);
|
console.error("Error adding marketplace API key:", error);
|
||||||
return {
|
return {
|
||||||
@ -1526,7 +1526,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -1541,7 +1541,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { input } = args;
|
const { input } = args;
|
||||||
|
|
||||||
// Обновляем данные пользователя (аватар, имя управляющего)
|
// Обновляем данные пользователя (аватар, имя управляющего)
|
||||||
const userUpdateData: { avatar?: string; managerName?: string } = {};
|
const userUpdateData: { avatar?: string; managerName?: string } = {};
|
||||||
if (input.avatar) {
|
if (input.avatar) {
|
||||||
@ -1550,14 +1550,14 @@ export const resolvers = {
|
|||||||
if (input.managerName) {
|
if (input.managerName) {
|
||||||
userUpdateData.managerName = input.managerName;
|
userUpdateData.managerName = input.managerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(userUpdateData).length > 0) {
|
if (Object.keys(userUpdateData).length > 0) {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
data: userUpdateData,
|
data: userUpdateData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подготавливаем данные для обновления организации
|
// Подготавливаем данные для обновления организации
|
||||||
const updateData: {
|
const updateData: {
|
||||||
phones?: object;
|
phones?: object;
|
||||||
@ -1565,20 +1565,20 @@ export const resolvers = {
|
|||||||
managementName?: string;
|
managementName?: string;
|
||||||
managementPost?: string;
|
managementPost?: string;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// Название организации больше не обновляется через профиль
|
// Название организации больше не обновляется через профиль
|
||||||
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
|
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
|
||||||
|
|
||||||
// Обновляем контактные данные в JSON поле phones
|
// Обновляем контактные данные в JSON поле phones
|
||||||
if (input.orgPhone) {
|
if (input.orgPhone) {
|
||||||
updateData.phones = [{ value: input.orgPhone, type: "main" }];
|
updateData.phones = [{ value: input.orgPhone, type: "main" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем email в JSON поле emails
|
// Обновляем email в JSON поле emails
|
||||||
if (input.email) {
|
if (input.email) {
|
||||||
updateData.emails = [{ value: input.email, type: "main" }];
|
updateData.emails = [{ value: input.email, type: "main" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем дополнительные контакты в custom полях
|
// Сохраняем дополнительные контакты в custom полях
|
||||||
// Пока добавим их как дополнительные JSON поля
|
// Пока добавим их как дополнительные JSON поля
|
||||||
const customContacts: {
|
const customContacts: {
|
||||||
@ -1592,13 +1592,13 @@ export const resolvers = {
|
|||||||
corrAccount?: string;
|
corrAccount?: string;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// managerName теперь сохраняется в поле пользователя, а не в JSON
|
// managerName теперь сохраняется в поле пользователя, а не в JSON
|
||||||
|
|
||||||
if (input.telegram) {
|
if (input.telegram) {
|
||||||
customContacts.telegram = input.telegram;
|
customContacts.telegram = input.telegram;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.whatsapp) {
|
if (input.whatsapp) {
|
||||||
customContacts.whatsapp = input.whatsapp;
|
customContacts.whatsapp = input.whatsapp;
|
||||||
}
|
}
|
||||||
@ -1616,7 +1616,7 @@ export const resolvers = {
|
|||||||
corrAccount: input.corrAccount,
|
corrAccount: input.corrAccount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
|
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
|
||||||
// В идеале нужно добавить отдельную таблицу для контактов
|
// В идеале нужно добавить отдельную таблицу для контактов
|
||||||
if (Object.keys(customContacts).length > 0) {
|
if (Object.keys(customContacts).length > 0) {
|
||||||
@ -1635,7 +1635,7 @@ export const resolvers = {
|
|||||||
// Получаем обновленного пользователя
|
// Получаем обновленного пользователя
|
||||||
const updatedUser = await prisma.user.findUnique({
|
const updatedUser = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -1671,7 +1671,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -1769,7 +1769,7 @@ export const resolvers = {
|
|||||||
// Получаем обновленного пользователя
|
// Получаем обновленного пользователя
|
||||||
const updatedUser = await prisma.user.findUnique({
|
const updatedUser = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -2930,22 +2930,22 @@ export const resolvers = {
|
|||||||
createProduct: async (
|
createProduct: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
args: {
|
args: {
|
||||||
input: {
|
input: {
|
||||||
name: string;
|
name: string;
|
||||||
article: string;
|
article: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
price: number;
|
price: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
dimensions?: string;
|
dimensions?: string;
|
||||||
material?: string;
|
material?: string;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
mainImage?: string;
|
mainImage?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
context: Context
|
context: Context
|
||||||
@ -3005,7 +3005,7 @@ export const resolvers = {
|
|||||||
isActive: args.input.isActive ?? true,
|
isActive: args.input.isActive ?? true,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
@ -3029,23 +3029,23 @@ export const resolvers = {
|
|||||||
updateProduct: async (
|
updateProduct: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
args: {
|
args: {
|
||||||
id: string;
|
id: string;
|
||||||
input: {
|
input: {
|
||||||
name: string;
|
name: string;
|
||||||
article: string;
|
article: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
price: number;
|
price: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
dimensions?: string;
|
dimensions?: string;
|
||||||
material?: string;
|
material?: string;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
mainImage?: string;
|
mainImage?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
context: Context
|
context: Context
|
||||||
@ -3115,7 +3115,7 @@ export const resolvers = {
|
|||||||
mainImage: args.input.mainImage,
|
mainImage: args.input.mainImage,
|
||||||
isActive: args.input.isActive ?? true,
|
isActive: args.input.isActive ?? true,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
@ -3400,7 +3400,7 @@ export const resolvers = {
|
|||||||
if (existingCartItem) {
|
if (existingCartItem) {
|
||||||
// Обновляем количество
|
// Обновляем количество
|
||||||
const newQuantity = existingCartItem.quantity + args.quantity;
|
const newQuantity = existingCartItem.quantity + args.quantity;
|
||||||
|
|
||||||
if (newQuantity > product.quantity) {
|
if (newQuantity > product.quantity) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -3940,7 +3940,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const employee = await prisma.employee.update({
|
const employee = await prisma.employee.update({
|
||||||
where: {
|
where: {
|
||||||
id: args.id,
|
id: args.id,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
@ -4002,7 +4002,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.employee.delete({
|
await prisma.employee.delete({
|
||||||
where: {
|
where: {
|
||||||
id: args.id,
|
id: args.id,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
@ -4150,7 +4150,7 @@ export const resolvers = {
|
|||||||
if (parent.users) {
|
if (parent.users) {
|
||||||
return parent.users;
|
return parent.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе загружаем отдельно
|
// Иначе загружаем отдельно
|
||||||
return await prisma.user.findMany({
|
return await prisma.user.findMany({
|
||||||
where: { organizationId: parent.id },
|
where: { organizationId: parent.id },
|
||||||
@ -4197,7 +4197,7 @@ export const resolvers = {
|
|||||||
if (parent.organization) {
|
if (parent.organization) {
|
||||||
return parent.organization;
|
return parent.organization;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе загружаем отдельно если есть organizationId
|
// Иначе загружаем отдельно если есть organizationId
|
||||||
if (parent.organizationId) {
|
if (parent.organizationId) {
|
||||||
return await prisma.organization.findUnique({
|
return await prisma.organization.findUnique({
|
||||||
@ -4514,14 +4514,14 @@ const adminQueries = {
|
|||||||
|
|
||||||
const limit = args.limit || 50;
|
const limit = args.limit || 50;
|
||||||
const offset = args.offset || 0;
|
const offset = args.offset || 0;
|
||||||
|
|
||||||
// Строим условие поиска
|
// Строим условие поиска
|
||||||
const whereCondition: Prisma.UserWhereInput = args.search
|
const whereCondition: Prisma.UserWhereInput = args.search
|
||||||
? {
|
? {
|
||||||
OR: [
|
OR: [
|
||||||
{ phone: { contains: args.search, mode: "insensitive" } },
|
{ phone: { contains: args.search, mode: "insensitive" } },
|
||||||
{ managerName: { contains: args.search, mode: "insensitive" } },
|
{ managerName: { contains: args.search, mode: "insensitive" } },
|
||||||
{
|
{
|
||||||
organization: {
|
organization: {
|
||||||
OR: [
|
OR: [
|
||||||
{ name: { contains: args.search, mode: "insensitive" } },
|
{ name: { contains: args.search, mode: "insensitive" } },
|
||||||
@ -4589,7 +4589,7 @@ const adminMutations = {
|
|||||||
args.password,
|
args.password,
|
||||||
admin.password
|
admin.password
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -4605,7 +4605,7 @@ const adminMutations = {
|
|||||||
|
|
||||||
// Создать токен
|
// Создать токен
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
adminId: admin.id,
|
adminId: admin.id,
|
||||||
username: admin.username,
|
username: admin.username,
|
||||||
type: "admin",
|
type: "admin",
|
||||||
|
@ -246,6 +246,7 @@ export const typeDefs = gql`
|
|||||||
type ApiKey {
|
type ApiKey {
|
||||||
id: ID!
|
id: ID!
|
||||||
marketplace: MarketplaceType!
|
marketplace: MarketplaceType!
|
||||||
|
apiKey: String!
|
||||||
isActive: Boolean!
|
isActive: Boolean!
|
||||||
validationData: JSON
|
validationData: JSON
|
||||||
createdAt: DateTime!
|
createdAt: DateTime!
|
||||||
|
@ -43,7 +43,7 @@ interface WildberriesCardsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface WildberriesCardFilter {
|
interface WildberriesCardFilter {
|
||||||
sort?: {
|
settings?: {
|
||||||
cursor?: {
|
cursor?: {
|
||||||
limit?: number
|
limit?: number
|
||||||
nmID?: number
|
nmID?: number
|
||||||
@ -55,61 +55,55 @@ interface WildberriesCardFilter {
|
|||||||
objectIDs?: number[]
|
objectIDs?: number[]
|
||||||
tagIDs?: number[]
|
tagIDs?: number[]
|
||||||
brandIDs?: number[]
|
brandIDs?: number[]
|
||||||
|
colorIDs?: number[]
|
||||||
|
sizeIDs?: number[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WildberriesService {
|
export class WildberriesService {
|
||||||
private static baseUrl = 'https://marketplace-api.wildberries.ru'
|
|
||||||
private static contentUrl = 'https://content-api.wildberries.ru'
|
private static contentUrl = 'https://content-api.wildberries.ru'
|
||||||
|
private static publicUrl = 'https://public-api.wildberries.ru'
|
||||||
|
private static supplierUrl = 'https://suppliers-api.wildberries.ru'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить список складов WB
|
* Получение карточек товаров через Content API v2
|
||||||
*/
|
|
||||||
static async getWarehouses(apiKey: string): Promise<WildberriesWarehouse[]> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/v2/warehouses`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': apiKey,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: WildberriesWarehousesResponse = await response.json()
|
|
||||||
return data.data || []
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching WB warehouses:', error)
|
|
||||||
throw new Error('Ошибка получения складов Wildberries')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить карточки товаров
|
|
||||||
*/
|
*/
|
||||||
static async getCards(apiKey: string, filter?: WildberriesCardFilter): Promise<WildberriesCard[]> {
|
static async getCards(apiKey: string, filter?: WildberriesCardFilter): Promise<WildberriesCard[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.contentUrl}/content/v1/cards/cursor/list`, {
|
console.log('Calling WB Content API v2 with filter:', filter)
|
||||||
|
|
||||||
|
const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': apiKey,
|
'Authorization': apiKey,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(filter || {
|
body: JSON.stringify(filter || {
|
||||||
sort: {
|
settings: {
|
||||||
cursor: {
|
cursor: {
|
||||||
limit: 100
|
limit: 100
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
withPhoto: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log(`${this.contentUrl}/content/v2/get/cards/list`, response.status, response.statusText)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
const errorText = await response.text()
|
||||||
|
let errorData
|
||||||
|
try {
|
||||||
|
errorData = JSON.parse(errorText)
|
||||||
|
} catch {
|
||||||
|
errorData = { message: errorText }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('WB API Error Response:', errorData)
|
||||||
|
throw new Error(`WB API Error: ${response.status} - ${response.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: WildberriesCardsResponse = await response.json()
|
const data: WildberriesCardsResponse = await response.json()
|
||||||
@ -121,16 +115,17 @@ export class WildberriesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Поиск карточек товаров по тексту
|
* Поиск карточек товаров
|
||||||
*/
|
*/
|
||||||
static async searchCards(apiKey: string, searchText: string, limit: number = 100): Promise<WildberriesCard[]> {
|
static async searchCards(apiKey: string, searchTerm: string, limit = 50): Promise<WildberriesCard[]> {
|
||||||
const filter: WildberriesCardFilter = {
|
const filter: WildberriesCardFilter = {
|
||||||
sort: {
|
settings: {
|
||||||
cursor: {
|
cursor: {
|
||||||
limit
|
limit
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
textSearch: searchText
|
textSearch: searchTerm,
|
||||||
|
withPhoto: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,63 +134,70 @@ export class WildberriesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Валидация API ключа WB
|
* Получение всех карточек товаров с пагинацией
|
||||||
|
*/
|
||||||
|
static async getAllCards(apiKey: string, limit = 100): Promise<WildberriesCard[]> {
|
||||||
|
const filter: WildberriesCardFilter = {
|
||||||
|
settings: {
|
||||||
|
cursor: {
|
||||||
|
limit
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
withPhoto: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getCards(apiKey, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение складов WB
|
||||||
|
*/
|
||||||
|
static async getWarehouses(apiKey: string): Promise<WildberriesWarehouse[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.supplierUrl}/api/v3/warehouses`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': apiKey,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: WildberriesWarehousesResponse = await response.json()
|
||||||
|
return data.data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching warehouses:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка валидности API ключа
|
||||||
*/
|
*/
|
||||||
static async validateApiKey(apiKey: string): Promise<boolean> {
|
static async validateApiKey(apiKey: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.getWarehouses(apiKey)
|
const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
|
||||||
return true
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': apiKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
settings: {
|
||||||
|
cursor: {
|
||||||
|
limit: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.ok
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('WB API key validation failed:', error)
|
console.error('Error validating API key:', error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить информацию о поставке
|
|
||||||
*/
|
|
||||||
static async getSupplyInfo(apiKey: string, supplyId: string): Promise<unknown> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/v3/supplies/${supplyId}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': apiKey,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching WB supply info:', error)
|
|
||||||
throw new Error('Ошибка получения информации о поставке')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить список поставок
|
|
||||||
*/
|
|
||||||
static async getSupplies(apiKey: string, limit: number = 1000, next: number = 0): Promise<unknown> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/v3/supplies?limit=${limit}&next=${next}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': apiKey,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching WB supplies:', error)
|
|
||||||
throw new Error('Ошибка получения списка поставок')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user