advrt
This commit is contained in:
37
deploy.sh
37
deploy.sh
@ -1,37 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "🚀 Starting deployment to new.sferav.ru..."
|
|
||||||
|
|
||||||
# Остановка предыдущей версии
|
|
||||||
echo "⏹️ Stopping previous version..."
|
|
||||||
docker-compose -f docker-compose.prod.yml down
|
|
||||||
|
|
||||||
# Очистка неиспользуемых образов
|
|
||||||
echo "🧹 Cleaning up unused images..."
|
|
||||||
docker image prune -f
|
|
||||||
|
|
||||||
# Сборка и запуск новой версии
|
|
||||||
echo "🔨 Building and starting new version..."
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
|
|
||||||
# Ожидание запуска
|
|
||||||
echo "⏳ Waiting for application to start..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Проверка здоровья
|
|
||||||
echo "🏥 Checking application health..."
|
|
||||||
for i in {1..30}; do
|
|
||||||
if curl -f http://127.0.0.1:3017/api/health > /dev/null 2>&1; then
|
|
||||||
echo "✅ Application is healthy!"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo "⏳ Attempt $i/30 - waiting for health check..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
# Проверка статуса контейнера
|
|
||||||
echo "📊 Container status:"
|
|
||||||
docker-compose -f docker-compose.prod.yml ps
|
|
||||||
|
|
||||||
echo "🎉 Deployment completed!"
|
|
||||||
echo "🌐 Application is available at: https://new.sferav.ru"
|
|
@ -1,36 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- SMS_AERO_EMAIL=${SMS_AERO_EMAIL}
|
|
||||||
- SMS_AERO_API_KEY=${SMS_AERO_API_KEY}
|
|
||||||
- SMS_AERO_API_URL=${SMS_AERO_API_URL}
|
|
||||||
- DADATA_API_KEY=${DADATA_API_KEY}
|
|
||||||
- DADATA_API_URL=${DADATA_API_URL}
|
|
||||||
- WILDBERRIES_API_URL=${WILDBERRIES_API_URL}
|
|
||||||
- OZON_API_URL=${OZON_API_URL}
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- SMS_DEV_MODE=${SMS_DEV_MODE}
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:3017:3000" # Привязка только к localhost
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- PORT=3000
|
|
||||||
- HOSTNAME=0.0.0.0
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
|
||||||
timeout: 10s
|
|
||||||
interval: 30s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
# Логирование
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
@ -463,6 +463,10 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
|
|||||||
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set())
|
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set())
|
||||||
const [expandedCampaigns, setExpandedCampaigns] = useState<Set<number>>(new Set())
|
const [expandedCampaigns, setExpandedCampaigns] = useState<Set<number>>(new Set())
|
||||||
|
|
||||||
|
// Состояния для фильтрации графика
|
||||||
|
const [showWbAds, setShowWbAds] = useState(true)
|
||||||
|
const [showExternalAds, setShowExternalAds] = useState(true)
|
||||||
|
|
||||||
// Состояние для формы добавления внешней рекламы
|
// Состояние для формы добавления внешней рекламы
|
||||||
const [showAddForm, setShowAddForm] = useState<string | null>(null)
|
const [showAddForm, setShowAddForm] = useState<string | null>(null)
|
||||||
const [newExternalAd, setNewExternalAd] = useState({
|
const [newExternalAd, setNewExternalAd] = useState({
|
||||||
@ -805,12 +809,13 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
|
|||||||
const result = Array.from(dailyMap.values())
|
const result = Array.from(dailyMap.values())
|
||||||
|
|
||||||
if (externalAdsData?.getExternalAds?.success && externalAdsData.getExternalAds.externalAds) {
|
if (externalAdsData?.getExternalAds?.success && externalAdsData.getExternalAds.externalAds) {
|
||||||
|
// Сначала обрабатываем существующие дни
|
||||||
result.forEach(day => {
|
result.forEach(day => {
|
||||||
const externalAdsForDay = externalAdsData.getExternalAds.externalAds.filter(
|
const externalAdsForDay = externalAdsData.getExternalAds.externalAds.filter(
|
||||||
(ad: ExternalAd & { date: string; nmId: string }) => ad.date === day.date
|
(ad: ExternalAd & { date: string; nmId: string }) => ad.date === day.date
|
||||||
)
|
)
|
||||||
|
|
||||||
if (externalAdsForDay.length > 0 && day.products.length > 0) {
|
if (externalAdsForDay.length > 0) {
|
||||||
// Группируем внешнюю рекламу по nmId товара
|
// Группируем внешнюю рекламу по nmId товара
|
||||||
const adsByProduct = externalAdsForDay.reduce((acc: Record<string, ExternalAd[]>, ad: ExternalAd & { date: string; nmId: string }) => {
|
const adsByProduct = externalAdsForDay.reduce((acc: Record<string, ExternalAd[]>, ad: ExternalAd & { date: string; nmId: string }) => {
|
||||||
if (!acc[ad.nmId]) acc[ad.nmId] = []
|
if (!acc[ad.nmId]) acc[ad.nmId] = []
|
||||||
@ -824,14 +829,88 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
// Добавляем внешнюю рекламу к соответствующим товарам
|
// Добавляем внешнюю рекламу к соответствующим товарам или создаем новые товары
|
||||||
day.products.forEach(product => {
|
Object.keys(adsByProduct).forEach(nmIdStr => {
|
||||||
if (adsByProduct[product.nmId.toString()]) {
|
const nmId = parseInt(nmIdStr)
|
||||||
product.advertising.externalAds = adsByProduct[product.nmId.toString()]
|
let existingProduct = day.products.find(p => p.nmId === nmId)
|
||||||
|
|
||||||
|
if (!existingProduct) {
|
||||||
|
// Создаем новый товар только с внешней рекламой
|
||||||
|
existingProduct = {
|
||||||
|
nmId: nmId,
|
||||||
|
name: `Товар ${nmId}`, // Будет обновлено при загрузке фотографий
|
||||||
|
totalViews: 0,
|
||||||
|
totalClicks: 0,
|
||||||
|
totalCost: 0,
|
||||||
|
totalOrders: 0,
|
||||||
|
totalRevenue: 0,
|
||||||
|
advertising: {
|
||||||
|
wbCampaigns: [],
|
||||||
|
externalAds: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
day.products.push(existingProduct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingProduct.advertising.externalAds = adsByProduct[nmIdStr]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Теперь обрабатываем дни, которых нет в ВБ кампаниях, но есть внешняя реклама
|
||||||
|
const existingDates = new Set(result.map(day => day.date))
|
||||||
|
const externalAdsByDate = externalAdsData.getExternalAds.externalAds.reduce((acc: Record<string, Array<ExternalAd & { date: string; nmId: string }>>, ad: ExternalAd & { date: string; nmId: string }) => {
|
||||||
|
if (!acc[ad.date]) acc[ad.date] = []
|
||||||
|
acc[ad.date].push(ad)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
Object.keys(externalAdsByDate).forEach(dateStr => {
|
||||||
|
if (!existingDates.has(dateStr)) {
|
||||||
|
// Создаем новый день только с товарами, у которых есть внешняя реклама
|
||||||
|
const newDay: DailyAdvertisingData = {
|
||||||
|
date: dateStr,
|
||||||
|
totalSum: 0,
|
||||||
|
totalOrders: 0,
|
||||||
|
totalRevenue: 0,
|
||||||
|
products: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Группируем внешнюю рекламу по nmId товара
|
||||||
|
const adsByProduct = externalAdsByDate[dateStr].reduce((acc: Record<string, ExternalAd[]>, ad) => {
|
||||||
|
if (!acc[ad.nmId]) acc[ad.nmId] = []
|
||||||
|
acc[ad.nmId].push({
|
||||||
|
id: ad.id,
|
||||||
|
name: ad.name,
|
||||||
|
url: ad.url,
|
||||||
|
cost: ad.cost,
|
||||||
|
clicks: ad.clicks || 0
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// Создаем товары с внешней рекламой
|
||||||
|
Object.keys(adsByProduct).forEach(nmIdStr => {
|
||||||
|
const nmId = parseInt(nmIdStr)
|
||||||
|
const product: ProductData = {
|
||||||
|
nmId: nmId,
|
||||||
|
name: `Товар ${nmId}`, // Будет обновлено при загрузке фотографий
|
||||||
|
totalViews: 0,
|
||||||
|
totalClicks: 0,
|
||||||
|
totalCost: 0,
|
||||||
|
totalOrders: 0,
|
||||||
|
totalRevenue: 0,
|
||||||
|
advertising: {
|
||||||
|
wbCampaigns: [],
|
||||||
|
externalAds: adsByProduct[nmIdStr]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newDay.products.push(product)
|
||||||
|
})
|
||||||
|
|
||||||
|
result.push(newDay)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем общие суммы дня (ВБ реклама + внешняя реклама)
|
// Обновляем общие суммы дня (ВБ реклама + внешняя реклама)
|
||||||
@ -929,13 +1008,12 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Обработчики для внешней рекламы
|
// Обработчики для внешней рекламы
|
||||||
const handleAddExternalAd = async (date: string, ad: Omit<ExternalAd, 'id'>) => {
|
const handleAddExternalAd = async (date: string, ad: Omit<ExternalAd, 'id'>, nmId?: string) => {
|
||||||
console.log('handleAddExternalAd called:', { date, ad })
|
console.log('handleAddExternalAd called:', { date, ad, nmId })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Находим nmId из первого товара дня (или можно передать отдельно)
|
// Используем переданный nmId или находим из первого товара дня как fallback
|
||||||
const dayData = dailyData.find(d => d.date === date)
|
const targetNmId = nmId || dailyData.find(d => d.date === date)?.products[0]?.nmId?.toString() || '0'
|
||||||
const nmId = dayData?.products[0]?.nmId?.toString() || '0'
|
|
||||||
|
|
||||||
await createExternalAd({
|
await createExternalAd({
|
||||||
variables: {
|
variables: {
|
||||||
@ -944,12 +1022,12 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
|
|||||||
url: ad.url,
|
url: ad.url,
|
||||||
cost: ad.cost,
|
cost: ad.cost,
|
||||||
date: date,
|
date: date,
|
||||||
nmId: nmId
|
nmId: targetNmId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('External ad created successfully')
|
console.log('External ad created successfully for nmId:', targetNmId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating external ad:', error)
|
console.error('Error creating external ad:', error)
|
||||||
}
|
}
|
||||||
@ -1237,6 +1315,34 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Чекбоксы для переключения типов рекламы */}
|
||||||
|
<div className="flex items-center gap-4 mb-3 p-2 bg-white/5 rounded">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="wb-ads"
|
||||||
|
checked={showWbAds}
|
||||||
|
onCheckedChange={setShowWbAds}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="wb-ads" className="text-xs text-white cursor-pointer flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 bg-blue-500 rounded"></div>
|
||||||
|
Реклама ВБ
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="external-ads"
|
||||||
|
checked={showExternalAds}
|
||||||
|
onCheckedChange={setShowExternalAds}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="external-ads" className="text-xs text-white cursor-pointer flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 bg-pink-500 rounded"></div>
|
||||||
|
Внешняя реклама
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="h-48">
|
<div className="h-48">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={spendingChartData}>
|
<BarChart data={spendingChartData}>
|
||||||
@ -1273,20 +1379,24 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
|
|||||||
return null
|
return null
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Bar
|
{showWbAds && (
|
||||||
dataKey="wbSum"
|
<Bar
|
||||||
fill="#3b82f6"
|
dataKey="wbSum"
|
||||||
name="ВБ реклама"
|
fill="#3b82f6"
|
||||||
radius={[2, 2, 0, 0]}
|
name="ВБ реклама"
|
||||||
opacity={0.8}
|
radius={[2, 2, 0, 0]}
|
||||||
/>
|
opacity={0.8}
|
||||||
<Bar
|
/>
|
||||||
dataKey="externalSum"
|
)}
|
||||||
fill="#ec4899"
|
{showExternalAds && (
|
||||||
name="Внешняя реклама"
|
<Bar
|
||||||
radius={[2, 2, 0, 0]}
|
dataKey="externalSum"
|
||||||
opacity={0.8}
|
fill="#ec4899"
|
||||||
/>
|
name="Внешняя реклама"
|
||||||
|
radius={[2, 2, 0, 0]}
|
||||||
|
opacity={0.8}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,23 +1,49 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
|
import { useQuery } from '@apollo/client'
|
||||||
|
import { GET_WB_WAREHOUSE_DATA } from '@/graphql/queries'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
Link,
|
Link,
|
||||||
Package,
|
Copy,
|
||||||
Eye,
|
Eye,
|
||||||
MousePointer,
|
MousePointer,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
DollarSign
|
DollarSign,
|
||||||
|
Search,
|
||||||
|
Package
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
interface WBStock {
|
||||||
|
nmId: number
|
||||||
|
vendorCode: string
|
||||||
|
title: string
|
||||||
|
brand: string
|
||||||
|
price: number
|
||||||
|
stocks: Array<{
|
||||||
|
warehouseId: number
|
||||||
|
warehouseName: string
|
||||||
|
quantity: number
|
||||||
|
quantityFull: number
|
||||||
|
inWayToClient: number
|
||||||
|
inWayFromClient: number
|
||||||
|
}>
|
||||||
|
totalQuantity: number
|
||||||
|
totalReserved: number
|
||||||
|
photos: any[]
|
||||||
|
mediaFiles: any[]
|
||||||
|
characteristics: any[]
|
||||||
|
subjectName: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ExternalAd {
|
interface ExternalAd {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@ -70,7 +96,7 @@ interface SimpleAdvertisingTableProps {
|
|||||||
dailyData: DailyAdvertisingData[]
|
dailyData: DailyAdvertisingData[]
|
||||||
productPhotos?: Map<number, string>
|
productPhotos?: Map<number, string>
|
||||||
generatedLinksData?: Record<string, GeneratedLink[]>
|
generatedLinksData?: Record<string, GeneratedLink[]>
|
||||||
onAddExternalAd?: (date: string, ad: Omit<ExternalAd, 'id'>) => void
|
onAddExternalAd?: (date: string, ad: Omit<ExternalAd, 'id'>, nmId?: string) => void
|
||||||
onRemoveExternalAd?: (date: string, adId: string) => void
|
onRemoveExternalAd?: (date: string, adId: string) => void
|
||||||
onUpdateExternalAd?: (date: string, adId: string, updates: Partial<ExternalAd>) => void
|
onUpdateExternalAd?: (date: string, adId: string, updates: Partial<ExternalAd>) => void
|
||||||
onGenerateLink?: (date: string, adId: string, adName: string, adUrl: string) => void
|
onGenerateLink?: (date: string, adId: string, adName: string, adUrl: string) => void
|
||||||
@ -85,16 +111,32 @@ export function SimpleAdvertisingTable({
|
|||||||
onUpdateExternalAd,
|
onUpdateExternalAd,
|
||||||
onGenerateLink
|
onGenerateLink
|
||||||
}: SimpleAdvertisingTableProps) {
|
}: SimpleAdvertisingTableProps) {
|
||||||
const [showWbAds, setShowWbAds] = useState(true)
|
const { user } = useAuth()
|
||||||
const [showExternalAds, setShowExternalAds] = useState(true)
|
|
||||||
const [expandedDays, setExpandedDays] = useState<Set<string>>(new Set())
|
|
||||||
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set())
|
|
||||||
const [showAddForm, setShowAddForm] = useState<string | null>(null)
|
const [showAddForm, setShowAddForm] = useState<string | null>(null)
|
||||||
|
const [showProductList, setShowProductList] = useState<string | null>(null)
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [filteredProducts, setFilteredProducts] = useState<WBStock[]>([])
|
||||||
const [newExternalAd, setNewExternalAd] = useState({
|
const [newExternalAd, setNewExternalAd] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
cost: ''
|
cost: ''
|
||||||
})
|
})
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<WBStock | null>(null)
|
||||||
|
|
||||||
|
// Получаем данные склада ВБ из кэша
|
||||||
|
const { data: warehouseData, loading: warehouseLoading, error: warehouseError } = useQuery(GET_WB_WAREHOUSE_DATA, {
|
||||||
|
skip: !user,
|
||||||
|
errorPolicy: 'all'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Вычисляем общие итоги для результирующей строки
|
||||||
|
const totalWbCost = dailyData.reduce((sum, day) =>
|
||||||
|
sum + day.products.reduce((daySum, product) => daySum + product.totalCost, 0), 0)
|
||||||
|
const totalExternalCost = dailyData.reduce((sum, day) =>
|
||||||
|
sum + day.products.reduce((daySum, product) =>
|
||||||
|
daySum + product.advertising.externalAds.reduce((adSum, ad) => adSum + ad.cost, 0), 0), 0)
|
||||||
|
const totalCost = totalWbCost + totalExternalCost
|
||||||
|
const totalOrders = dailyData.reduce((sum, day) => sum + day.totalOrders, 0)
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
const formatCurrency = (value: number) => {
|
||||||
if (value === 0) return '—'
|
if (value === 0) return '—'
|
||||||
@ -113,66 +155,102 @@ export function SimpleAdvertisingTable({
|
|||||||
return value > 0 ? new Intl.NumberFormat('ru-RU').format(value) : '—'
|
return value > 0 ? new Intl.NumberFormat('ru-RU').format(value) : '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleDay = (date: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
const newExpanded = new Set(expandedDays)
|
const date = new Date(dateString)
|
||||||
if (newExpanded.has(date)) {
|
return date.toLocaleDateString('ru-RU', {
|
||||||
newExpanded.delete(date)
|
day: '2-digit',
|
||||||
} else {
|
month: '2-digit',
|
||||||
newExpanded.add(date)
|
year: 'numeric'
|
||||||
}
|
})
|
||||||
setExpandedDays(newExpanded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleProduct = (date: string, nmId: number) => {
|
|
||||||
const key = `${date}-${nmId}`
|
|
||||||
const newExpanded = new Set(expandedProducts)
|
|
||||||
if (newExpanded.has(key)) {
|
|
||||||
newExpanded.delete(key)
|
|
||||||
} else {
|
|
||||||
newExpanded.add(key)
|
|
||||||
}
|
|
||||||
setExpandedProducts(newExpanded)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddExternalAdLocal = (date: string, nmId: number) => {
|
const handleAddExternalAdLocal = (productKey: string, date: string, nmId?: string) => {
|
||||||
if (newExternalAd.name && newExternalAd.url && newExternalAd.cost && onAddExternalAd) {
|
if (newExternalAd.name && newExternalAd.url && newExternalAd.cost && onAddExternalAd) {
|
||||||
|
console.log('Creating external ad for:', { productKey, date, nmId, selectedProduct })
|
||||||
|
|
||||||
onAddExternalAd(date, {
|
onAddExternalAd(date, {
|
||||||
name: newExternalAd.name,
|
name: newExternalAd.name,
|
||||||
url: newExternalAd.url,
|
url: newExternalAd.url,
|
||||||
cost: parseFloat(newExternalAd.cost) || 0
|
cost: parseFloat(newExternalAd.cost) || 0
|
||||||
})
|
}, nmId)
|
||||||
setNewExternalAd({ name: '', url: '', cost: '' })
|
setNewExternalAd({ name: '', url: '', cost: '' })
|
||||||
setShowAddForm(null)
|
setShowAddForm(null)
|
||||||
|
setSelectedProduct(null)
|
||||||
|
setShowProductList(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получаем все товары из кэша ВБ
|
||||||
|
const getAllProducts = (): WBStock[] => {
|
||||||
|
if (!warehouseData?.getWBWarehouseData?.success || !warehouseData.getWBWarehouseData.cache?.data) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedData = typeof warehouseData.getWBWarehouseData.cache.data === 'string'
|
||||||
|
? JSON.parse(warehouseData.getWBWarehouseData.cache.data)
|
||||||
|
: warehouseData.getWBWarehouseData.cache.data
|
||||||
|
|
||||||
|
return parsedData.stocks || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing warehouse data:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterProducts = (term: string) => {
|
||||||
|
const allProducts = getAllProducts()
|
||||||
|
|
||||||
|
if (!term.trim()) {
|
||||||
|
setFilteredProducts(allProducts)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = allProducts.filter(product =>
|
||||||
|
product.title.toLowerCase().includes(term.toLowerCase()) ||
|
||||||
|
product.brand.toLowerCase().includes(term.toLowerCase()) ||
|
||||||
|
product.vendorCode.toLowerCase().includes(term.toLowerCase()) ||
|
||||||
|
product.subjectName?.toLowerCase().includes(term.toLowerCase()) ||
|
||||||
|
product.nmId.toString().includes(term)
|
||||||
|
)
|
||||||
|
setFilteredProducts(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProductSelect = (product: WBStock, date: string) => {
|
||||||
|
setSelectedProduct(product)
|
||||||
|
setShowProductList(null)
|
||||||
|
setShowAddForm(`new-product-${date}-${product.nmId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShowProductList = (date: string) => {
|
||||||
|
setShowProductList(date)
|
||||||
|
const allProducts = getAllProducts()
|
||||||
|
setFilteredProducts(allProducts)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProductImage = (product: WBStock) => {
|
||||||
|
// Генерируем fallback URL как в оригинальном компоненте
|
||||||
|
const fallbackUrl = `https://basket-${String(product.nmId).slice(0, 2)}.wbbasket.ru/vol${String(product.nmId).slice(0, -5)}/part${String(product.nmId).slice(0, -3)}/${product.nmId}/images/big/1.webp`
|
||||||
|
|
||||||
|
// Проверяем photos
|
||||||
|
if (product.photos && product.photos.length > 0) {
|
||||||
|
const photo = product.photos[0] as any
|
||||||
|
return photo?.big || photo?.c516x688 || photo?.c246x328 || photo?.square || photo?.tm || fallbackUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем mediaFiles
|
||||||
|
if (product.mediaFiles && product.mediaFiles.length > 0) {
|
||||||
|
const media = product.mediaFiles[0] as any
|
||||||
|
return media?.big || media?.c516x688 || media?.c246x328 || media?.square || media?.tm || fallbackUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackUrl
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white/5 rounded-lg border border-white/10 overflow-hidden">
|
<div className="bg-white/5 rounded-lg border border-white/10 overflow-hidden">
|
||||||
{/* Фильтры */}
|
{/* Заголовок таблицы */}
|
||||||
<div className="p-4 border-b border-white/10">
|
|
||||||
<div className="flex gap-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id="wb-ads"
|
|
||||||
checked={showWbAds}
|
|
||||||
onCheckedChange={(checked) => setShowWbAds(checked === true)}
|
|
||||||
className="border-white/30"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="wb-ads" className="text-white/80 text-sm">Реклама ВБ</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id="external-ads"
|
|
||||||
checked={showExternalAds}
|
|
||||||
onCheckedChange={(checked) => setShowExternalAds(checked === true)}
|
|
||||||
className="border-white/30"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="external-ads" className="text-white/80 text-sm">Реклама внешняя</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Заголовок таблицы - как в Figma */}
|
|
||||||
<div className="bg-purple-600/20 border-b border-white/10">
|
<div className="bg-purple-600/20 border-b border-white/10">
|
||||||
<div className="grid grid-cols-5 gap-4 p-3 text-white text-sm font-medium">
|
<div className="grid grid-cols-5 gap-4 p-3 text-white text-sm font-medium">
|
||||||
<div>Дата</div>
|
<div>Дата</div>
|
||||||
@ -183,7 +261,18 @@ export function SimpleAdvertisingTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Строки таблицы с раскрывающимся содержимым */}
|
{/* Результирующая строка под шапкой */}
|
||||||
|
<div className="bg-green-600/20 border-b border-white/10">
|
||||||
|
<div className="grid grid-cols-5 gap-4 p-3 text-white text-sm font-bold">
|
||||||
|
<div>ИТОГО</div>
|
||||||
|
<div className="text-center">{formatCurrency(totalCost)}</div>
|
||||||
|
<div className="text-center">{totalOrders}</div>
|
||||||
|
<div className="text-center">{formatCurrency(totalWbCost)}</div>
|
||||||
|
<div className="text-center">{formatCurrency(totalExternalCost)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Строки таблицы - 2 уровня: день -> товары (всегда открыто) */}
|
||||||
<div className="max-h-96 overflow-y-auto">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
{dailyData.map((day) => {
|
{dailyData.map((day) => {
|
||||||
const dayWbCost = day.products.reduce((sum, product) => sum + product.totalCost, 0)
|
const dayWbCost = day.products.reduce((sum, product) => sum + product.totalCost, 0)
|
||||||
@ -191,100 +280,80 @@ export function SimpleAdvertisingTable({
|
|||||||
sum + product.advertising.externalAds.reduce((adSum, ad) => adSum + ad.cost, 0), 0)
|
sum + product.advertising.externalAds.reduce((adSum, ad) => adSum + ad.cost, 0), 0)
|
||||||
const dayTotalCost = dayWbCost + dayExternalCost
|
const dayTotalCost = dayWbCost + dayExternalCost
|
||||||
const dayOrders = day.totalOrders
|
const dayOrders = day.totalOrders
|
||||||
const isExpanded = expandedDays.has(day.date)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={day.date}>
|
<div key={day.date}>
|
||||||
{/* Основная строка дня - как в Figma */}
|
{/* Основная строка дня */}
|
||||||
<div
|
<div className="grid grid-cols-5 gap-4 p-3 border-b border-white/5 bg-white/5 text-white/80 text-sm font-medium">
|
||||||
className="grid grid-cols-5 gap-4 p-3 border-b border-white/5 hover:bg-white/5 text-white/80 text-sm cursor-pointer"
|
|
||||||
onClick={() => toggleDay(day.date)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
<span className="font-bold">{formatDate(day.date)}</span>
|
||||||
<span className="font-medium">{day.date}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">{formatCurrency(dayTotalCost)}</div>
|
<div className="text-center">{formatCurrency(dayTotalCost)}</div>
|
||||||
<div className="text-center">{dayOrders}</div>
|
<div className="text-center">{dayOrders}</div>
|
||||||
<div className="text-center">
|
<div className="text-center">{formatCurrency(dayWbCost)}</div>
|
||||||
{showWbAds ? formatCurrency(dayWbCost) : '—'}
|
<div className="text-center">{formatCurrency(dayExternalCost)}</div>
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
{showExternalAds ? formatCurrency(dayExternalCost) : '—'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Раскрывающееся содержимое с товарами */}
|
{/* Товары всегда видны - второй уровень */}
|
||||||
{isExpanded && (
|
<div className="border-b border-white/5">
|
||||||
<div className="bg-white/2 border-b border-white/5">
|
{day.products.map((product) => {
|
||||||
{day.products.map((product) => {
|
|
||||||
const productKey = `${day.date}-${product.nmId}`
|
const productKey = `${day.date}-${product.nmId}`
|
||||||
const isProductExpanded = expandedProducts.has(productKey)
|
|
||||||
const productExternalCost = product.advertising.externalAds.reduce((sum, ad) => sum + ad.cost, 0)
|
const productExternalCost = product.advertising.externalAds.reduce((sum, ad) => sum + ad.cost, 0)
|
||||||
|
const productTotalCost = product.totalCost + productExternalCost
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={productKey} className="ml-6">
|
<div key={productKey} className="ml-4">
|
||||||
{/* Строка товара */}
|
{/* Строка товара с многострочными ячейками */}
|
||||||
<div
|
<div className="grid grid-cols-5 gap-4 p-3 border-b border-white/5 text-white/70 text-sm">
|
||||||
className="grid grid-cols-5 gap-4 p-3 border-b border-white/5 hover:bg-white/5 text-white/70 text-sm cursor-pointer"
|
{/* Карточка товара */}
|
||||||
onClick={() => toggleProduct(day.date, product.nmId)}
|
<div className="flex items-start gap-2">
|
||||||
>
|
{productPhotos.has(product.nmId) && (
|
||||||
<div className="flex items-center gap-2">
|
<img
|
||||||
{isProductExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
src={productPhotos.get(product.nmId)}
|
||||||
<div className="flex items-center gap-2">
|
alt={product.name}
|
||||||
{productPhotos.has(product.nmId) && (
|
className="w-8 h-8 rounded object-cover flex-shrink-0"
|
||||||
<img
|
/>
|
||||||
src={productPhotos.get(product.nmId)}
|
)}
|
||||||
alt={product.name}
|
<div>
|
||||||
className="w-6 h-6 rounded object-cover"
|
<div className="text-white/90 text-xs font-medium">{product.name}</div>
|
||||||
/>
|
<div className="text-white/60 text-xs">#{product.nmId}</div>
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<div className="text-white/90 text-xs font-medium truncate max-w-32">{product.name}</div>
|
|
||||||
<div className="text-white/60 text-xs">#{product.nmId}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">{formatCurrency(product.totalCost + productExternalCost)}</div>
|
|
||||||
<div className="text-center">{product.totalOrders}</div>
|
|
||||||
<div className="text-center">
|
|
||||||
{showWbAds ? formatCurrency(product.totalCost) : '—'}
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
{showExternalAds ? formatCurrency(productExternalCost) : '—'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Раскрывающееся содержимое товара с кампаниями */}
|
{/* Общая сумма */}
|
||||||
{isProductExpanded && (
|
<div className="text-center self-start">{formatCurrency(productTotalCost)}</div>
|
||||||
<div className="ml-6 bg-white/1 border-b border-white/5">
|
|
||||||
{/* ВБ кампании */}
|
{/* Заказы */}
|
||||||
{showWbAds && product.advertising.wbCampaigns.map((campaign) => (
|
<div className="text-center self-start">{product.totalOrders}</div>
|
||||||
<div key={campaign.campaignId} className="grid grid-cols-5 gap-4 p-2 text-white/60 text-xs">
|
|
||||||
<div className="flex items-center gap-1">
|
{/* Реклама ВБ - многострочная ячейка */}
|
||||||
<Eye className="h-3 w-3 text-blue-400" />
|
<div className="text-center">
|
||||||
<span>ВБ #{campaign.campaignId}</span>
|
{product.advertising.wbCampaigns.length > 0 ? (
|
||||||
</div>
|
<div className="space-y-1">
|
||||||
<div className="text-center">{formatCurrency(campaign.cost)}</div>
|
{product.advertising.wbCampaigns.map((campaign, index) => (
|
||||||
<div className="text-center">{campaign.orders}</div>
|
<div key={campaign.campaignId} className="text-xs bg-blue-500/10 rounded p-1">
|
||||||
<div className="text-center">{formatCurrency(campaign.cost)}</div>
|
<div className="text-blue-400 font-medium">
|
||||||
<div className="text-center">—</div>
|
{index === 0 ? 'Авто' : index === 1 ? 'Фразы' : index === 2 ? 'Предмет' : `Тип ${campaign.campaignId}`}
|
||||||
|
</div>
|
||||||
|
<div className="text-white/80">{formatCurrency(campaign.cost)}</div>
|
||||||
|
<div className="text-white/60">{campaign.orders} зак.</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
<span className="text-white/40">—</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Внешняя реклама */}
|
{/* Реклама внешняя - многострочная ячейка с кнопками */}
|
||||||
{showExternalAds && product.advertising.externalAds.map((ad) => (
|
<div className="text-center">
|
||||||
<div key={ad.id} className="grid grid-cols-5 gap-4 p-2 text-white/60 text-xs">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-1">
|
{product.advertising.externalAds.map((ad) => (
|
||||||
<MousePointer className="h-3 w-3 text-purple-400" />
|
<div key={ad.id} className="text-xs bg-purple-500/10 rounded p-1">
|
||||||
<span className="truncate max-w-24">{ad.name}</span>
|
<div className="text-purple-400 font-medium truncate">{ad.name}</div>
|
||||||
</div>
|
<div className="text-white/80">{formatCurrency(ad.cost)}</div>
|
||||||
<div className="text-center">{formatCurrency(ad.cost)}</div>
|
<div className="text-white/60">{ad.clicks || 0} кликов</div>
|
||||||
<div className="text-center">—</div>
|
<div className="flex gap-1 justify-center mt-1">
|
||||||
<div className="text-center">—</div>
|
|
||||||
<div className="text-center flex items-center justify-center gap-1">
|
|
||||||
{formatCurrency(ad.cost)}
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{onGenerateLink && (
|
{onGenerateLink && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -293,9 +362,10 @@ export function SimpleAdvertisingTable({
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onGenerateLink(day.date, ad.id, ad.name, ad.url)
|
onGenerateLink(day.date, ad.id, ad.name, ad.url)
|
||||||
}}
|
}}
|
||||||
className="h-4 w-4 p-0 text-blue-400 hover:bg-blue-500/20"
|
className="h-5 w-5 p-0 text-blue-400 hover:bg-blue-500/20"
|
||||||
|
title="Скопировать ссылку"
|
||||||
>
|
>
|
||||||
<Link className="h-2 w-2" />
|
<Copy className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onRemoveExternalAd && (
|
{onRemoveExternalAd && (
|
||||||
@ -306,22 +376,21 @@ export function SimpleAdvertisingTable({
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onRemoveExternalAd(day.date, ad.id)
|
onRemoveExternalAd(day.date, ad.id)
|
||||||
}}
|
}}
|
||||||
className="h-4 w-4 p-0 text-red-400 hover:bg-red-500/20"
|
className="h-5 w-5 p-0 text-red-400 hover:bg-red-500/20"
|
||||||
|
title="Удалить"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-2 w-2" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Кнопка добавления внешней рекламы */}
|
{/* Инлайн форма добавления внешней рекламы */}
|
||||||
{onAddExternalAd && (
|
{onAddExternalAd && (
|
||||||
<div className="grid grid-cols-5 gap-4 p-2">
|
<div>
|
||||||
<div className="col-span-5">
|
|
||||||
{showAddForm === productKey ? (
|
{showAddForm === productKey ? (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="bg-white/5 rounded p-2 space-y-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Название"
|
placeholder="Название"
|
||||||
value={newExternalAd.name}
|
value={newExternalAd.name}
|
||||||
@ -339,44 +408,201 @@ export function SimpleAdvertisingTable({
|
|||||||
type="number"
|
type="number"
|
||||||
value={newExternalAd.cost}
|
value={newExternalAd.cost}
|
||||||
onChange={(e) => setNewExternalAd(prev => ({ ...prev, cost: e.target.value }))}
|
onChange={(e) => setNewExternalAd(prev => ({ ...prev, cost: e.target.value }))}
|
||||||
className="h-6 w-20 bg-white/10 border-white/20 text-white text-xs"
|
className="h-6 bg-white/10 border-white/20 text-white text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<div className="flex gap-1">
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => handleAddExternalAdLocal(day.date, product.nmId)}
|
size="sm"
|
||||||
className="h-6 bg-green-600 hover:bg-green-700 text-white text-xs px-2"
|
onClick={() => handleAddExternalAdLocal(productKey, day.date)}
|
||||||
>
|
className="h-6 bg-green-600 hover:bg-green-700 text-white text-xs px-2 flex-1"
|
||||||
Добавить
|
>
|
||||||
</Button>
|
Добавить
|
||||||
<Button
|
</Button>
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => setShowAddForm(null)}
|
size="sm"
|
||||||
variant="ghost"
|
onClick={() => setShowAddForm(null)}
|
||||||
className="h-6 text-white/60 hover:bg-white/10 text-xs px-2"
|
variant="ghost"
|
||||||
>
|
className="h-6 text-white/60 hover:bg-white/10 text-xs px-2"
|
||||||
Отмена
|
>
|
||||||
</Button>
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<button
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowAddForm(productKey)}
|
onClick={() => setShowAddForm(productKey)}
|
||||||
className="h-6 px-2 bg-purple-600 hover:bg-purple-700 text-white text-xs"
|
className="w-full h-6 px-2 bg-gradient-to-r from-purple-500/20 to-purple-600/20 border border-purple-500/30 rounded text-purple-300 hover:from-purple-500/30 hover:to-purple-600/30 hover:border-purple-400/50 hover:text-purple-200 transition-all duration-200 text-xs flex items-center justify-center gap-1 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<Plus className="h-2 w-2 mr-1" />
|
<Plus className="h-3 w-3" />
|
||||||
Добавить внешнюю рекламу
|
Добавить
|
||||||
</Button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
)}
|
{/* Кнопка добавления рекламы для нового товара */}
|
||||||
|
{onAddExternalAd && (
|
||||||
|
<div className="ml-4 p-3 border-b border-white/5">
|
||||||
|
{showProductList === day.date ? (
|
||||||
|
<div className="bg-white/5 rounded p-3 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-white font-medium text-sm">Выберите товар для рекламы</h4>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setShowProductList(null)
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
className="text-white/60 hover:bg-white/10 h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поиск среди загруженных товаров */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Search className="h-4 w-4 text-white/60" />
|
||||||
|
<Input
|
||||||
|
placeholder="Поиск среди товаров..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
filterProducts(e.target.value)
|
||||||
|
}}
|
||||||
|
className="bg-white/10 border-white/20 text-white text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{warehouseLoading ? (
|
||||||
|
<div className="text-center text-white/60 text-sm py-4">
|
||||||
|
Загрузка товаров...
|
||||||
|
</div>
|
||||||
|
) : warehouseError ? (
|
||||||
|
<div className="text-center text-red-400 text-sm py-4">
|
||||||
|
Ошибка загрузки товаров: {warehouseError.message}
|
||||||
|
</div>
|
||||||
|
) : filteredProducts.length > 0 ? (
|
||||||
|
<div className="max-h-64 overflow-y-auto space-y-1">
|
||||||
|
{filteredProducts.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product.nmId}
|
||||||
|
onClick={() => handleProductSelect(product, day.date)}
|
||||||
|
className="flex items-center gap-3 p-3 bg-white/5 rounded hover:bg-white/10 cursor-pointer transition-colors border border-white/10"
|
||||||
|
>
|
||||||
|
{getProductImage(product) && (
|
||||||
|
<img
|
||||||
|
src={getProductImage(product)!}
|
||||||
|
alt={product.title}
|
||||||
|
className="w-12 h-12 rounded object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-white text-sm font-medium truncate mb-1">
|
||||||
|
{product.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-white/60 text-xs space-y-0.5">
|
||||||
|
<div>{product.brand} • #{product.nmId}</div>
|
||||||
|
<div>Артикул: {product.vendorCode}</div>
|
||||||
|
<div className="text-green-400">На складе: {product.totalQuantity} шт.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-white/60 text-sm py-4">
|
||||||
|
{searchTerm ? 'Товары не найдены' : 'Нет доступных товаров'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : showAddForm?.startsWith(`new-product-${day.date}`) ? (
|
||||||
|
<div className="bg-white/5 rounded p-3 space-y-3">
|
||||||
|
{selectedProduct && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-white/10 rounded border border-white/20">
|
||||||
|
{getProductImage(selectedProduct) && (
|
||||||
|
<img
|
||||||
|
src={getProductImage(selectedProduct)!}
|
||||||
|
alt={selectedProduct.title}
|
||||||
|
className="w-12 h-12 rounded object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-white text-sm font-medium truncate mb-1">
|
||||||
|
{selectedProduct.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-white/60 text-xs space-y-0.5">
|
||||||
|
<div>{selectedProduct.brand} • #{selectedProduct.nmId}</div>
|
||||||
|
<div>Артикул: {selectedProduct.vendorCode}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Название рекламы"
|
||||||
|
value={newExternalAd.name}
|
||||||
|
onChange={(e) => setNewExternalAd(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
className="bg-white/10 border-white/20 text-white text-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="URL рекламы"
|
||||||
|
value={newExternalAd.url}
|
||||||
|
onChange={(e) => setNewExternalAd(prev => ({ ...prev, url: e.target.value }))}
|
||||||
|
className="bg-white/10 border-white/20 text-white text-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Стоимость"
|
||||||
|
type="number"
|
||||||
|
value={newExternalAd.cost}
|
||||||
|
onChange={(e) => setNewExternalAd(prev => ({ ...prev, cost: e.target.value }))}
|
||||||
|
className="bg-white/10 border-white/20 text-white text-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => selectedProduct && handleAddExternalAdLocal(
|
||||||
|
`new-product-${day.date}-${selectedProduct.nmId}`,
|
||||||
|
day.date,
|
||||||
|
selectedProduct.nmId.toString()
|
||||||
|
)}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white text-sm flex-1"
|
||||||
|
>
|
||||||
|
Создать рекламу
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddForm(null)
|
||||||
|
setSelectedProduct(null)
|
||||||
|
setNewExternalAd({ name: '', url: '', cost: '' })
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
className="text-white/60 hover:bg-white/10 text-sm"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => handleShowProductList(day.date)}
|
||||||
|
className="w-full h-8 px-3 bg-gradient-to-r from-green-500/20 to-green-600/20 border border-green-500/30 rounded text-green-300 hover:from-green-500/30 hover:to-green-600/30 hover:border-green-400/50 hover:text-green-200 transition-all duration-200 text-sm flex items-center justify-center gap-2 backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<Package className="h-4 w-4" />
|
||||||
|
Добавить рекламу для товара
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
Reference in New Issue
Block a user