From 50c1ab0145fa654c9df9313736ccbdad5b8ba8be Mon Sep 17 00:00:00 2001 From: Bivekich Date: Wed, 30 Jul 2025 13:38:12 +0300 Subject: [PATCH] advrt --- deploy.sh | 37 -- docker-compose.prod.yml | 36 -- .../seller-statistics/advertising-tab.tsx | 162 ++++- .../simple-advertising-table.tsx | 566 ++++++++++++------ 4 files changed, 532 insertions(+), 269 deletions(-) delete mode 100644 deploy.sh delete mode 100644 docker-compose.prod.yml diff --git a/deploy.sh b/deploy.sh deleted file mode 100644 index 5f63f1b..0000000 --- a/deploy.sh +++ /dev/null @@ -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" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 24560a3..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/src/components/seller-statistics/advertising-tab.tsx b/src/components/seller-statistics/advertising-tab.tsx index 82b65a3..1b68555 100644 --- a/src/components/seller-statistics/advertising-tab.tsx +++ b/src/components/seller-statistics/advertising-tab.tsx @@ -463,6 +463,10 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD const [expandedProducts, setExpandedProducts] = useState>(new Set()) const [expandedCampaigns, setExpandedCampaigns] = useState>(new Set()) + // Состояния для фильтрации графика + const [showWbAds, setShowWbAds] = useState(true) + const [showExternalAds, setShowExternalAds] = useState(true) + // Состояние для формы добавления внешней рекламы const [showAddForm, setShowAddForm] = useState(null) const [newExternalAd, setNewExternalAd] = useState({ @@ -805,12 +809,13 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD const result = Array.from(dailyMap.values()) if (externalAdsData?.getExternalAds?.success && externalAdsData.getExternalAds.externalAds) { + // Сначала обрабатываем существующие дни result.forEach(day => { const externalAdsForDay = externalAdsData.getExternalAds.externalAds.filter( (ad: ExternalAd & { date: string; nmId: string }) => ad.date === day.date ) - if (externalAdsForDay.length > 0 && day.products.length > 0) { + if (externalAdsForDay.length > 0) { // Группируем внешнюю рекламу по nmId товара const adsByProduct = externalAdsForDay.reduce((acc: Record, ad: ExternalAd & { date: string; nmId: string }) => { if (!acc[ad.nmId]) acc[ad.nmId] = [] @@ -824,14 +829,88 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD return acc }, {}) - // Добавляем внешнюю рекламу к соответствующим товарам - day.products.forEach(product => { - if (adsByProduct[product.nmId.toString()]) { - product.advertising.externalAds = adsByProduct[product.nmId.toString()] + // Добавляем внешнюю рекламу к соответствующим товарам или создаем новые товары + Object.keys(adsByProduct).forEach(nmIdStr => { + const nmId = parseInt(nmIdStr) + 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>, 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, 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) => { - console.log('handleAddExternalAd called:', { date, ad }) + const handleAddExternalAd = async (date: string, ad: Omit, nmId?: string) => { + console.log('handleAddExternalAd called:', { date, ad, nmId }) try { - // Находим nmId из первого товара дня (или можно передать отдельно) - const dayData = dailyData.find(d => d.date === date) - const nmId = dayData?.products[0]?.nmId?.toString() || '0' + // Используем переданный nmId или находим из первого товара дня как fallback + const targetNmId = nmId || dailyData.find(d => d.date === date)?.products[0]?.nmId?.toString() || '0' await createExternalAd({ variables: { @@ -944,12 +1022,12 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD url: ad.url, cost: ad.cost, date: date, - nmId: nmId + nmId: targetNmId } } }) - console.log('External ad created successfully') + console.log('External ad created successfully for nmId:', targetNmId) } catch (error) { console.error('Error creating external ad:', error) } @@ -1237,6 +1315,34 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD + {/* Чекбоксы для переключения типов рекламы */} +
+
+ + +
+
+ + +
+
+
@@ -1273,20 +1379,24 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD return null }} /> - - + {showWbAds && ( + + )} + {showExternalAds && ( + + )}
diff --git a/src/components/seller-statistics/simple-advertising-table.tsx b/src/components/seller-statistics/simple-advertising-table.tsx index 1a110a0..7cab039 100644 --- a/src/components/seller-statistics/simple-advertising-table.tsx +++ b/src/components/seller-statistics/simple-advertising-table.tsx @@ -1,23 +1,49 @@ "use client" import React, { useState } from 'react' -import { Checkbox } from '@/components/ui/checkbox' -import { Label } from '@/components/ui/label' import { Button } from '@/components/ui/button' 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 { ChevronDown, ChevronRight, Plus, Trash2, Link, - Package, + Copy, Eye, MousePointer, ShoppingCart, - DollarSign + DollarSign, + Search, + Package } 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 { id: string name: string @@ -70,7 +96,7 @@ interface SimpleAdvertisingTableProps { dailyData: DailyAdvertisingData[] productPhotos?: Map generatedLinksData?: Record - onAddExternalAd?: (date: string, ad: Omit) => void + onAddExternalAd?: (date: string, ad: Omit, nmId?: string) => void onRemoveExternalAd?: (date: string, adId: string) => void onUpdateExternalAd?: (date: string, adId: string, updates: Partial) => void onGenerateLink?: (date: string, adId: string, adName: string, adUrl: string) => void @@ -85,16 +111,32 @@ export function SimpleAdvertisingTable({ onUpdateExternalAd, onGenerateLink }: SimpleAdvertisingTableProps) { - const [showWbAds, setShowWbAds] = useState(true) - const [showExternalAds, setShowExternalAds] = useState(true) - const [expandedDays, setExpandedDays] = useState>(new Set()) - const [expandedProducts, setExpandedProducts] = useState>(new Set()) + const { user } = useAuth() const [showAddForm, setShowAddForm] = useState(null) + const [showProductList, setShowProductList] = useState(null) + const [searchTerm, setSearchTerm] = useState('') + const [filteredProducts, setFilteredProducts] = useState([]) const [newExternalAd, setNewExternalAd] = useState({ name: '', url: '', cost: '' }) + const [selectedProduct, setSelectedProduct] = useState(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) => { if (value === 0) return '—' @@ -113,66 +155,102 @@ export function SimpleAdvertisingTable({ return value > 0 ? new Intl.NumberFormat('ru-RU').format(value) : '—' } - const toggleDay = (date: string) => { - const newExpanded = new Set(expandedDays) - if (newExpanded.has(date)) { - newExpanded.delete(date) - } else { - newExpanded.add(date) - } - setExpandedDays(newExpanded) + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }) } - 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) { + console.log('Creating external ad for:', { productKey, date, nmId, selectedProduct }) + onAddExternalAd(date, { name: newExternalAd.name, url: newExternalAd.url, cost: parseFloat(newExternalAd.cost) || 0 - }) + }, nmId) setNewExternalAd({ name: '', url: '', cost: '' }) 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 (
- {/* Фильтры */} -
-
-
- setShowWbAds(checked === true)} - className="border-white/30" - /> - -
-
- setShowExternalAds(checked === true)} - className="border-white/30" - /> - -
-
-
- - {/* Заголовок таблицы - как в Figma */} + {/* Заголовок таблицы */}
Дата
@@ -183,7 +261,18 @@ export function SimpleAdvertisingTable({
- {/* Строки таблицы с раскрывающимся содержимым */} + {/* Результирующая строка под шапкой */} +
+
+
ИТОГО
+
{formatCurrency(totalCost)}
+
{totalOrders}
+
{formatCurrency(totalWbCost)}
+
{formatCurrency(totalExternalCost)}
+
+
+ + {/* Строки таблицы - 2 уровня: день -> товары (всегда открыто) */}
{dailyData.map((day) => { 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) const dayTotalCost = dayWbCost + dayExternalCost const dayOrders = day.totalOrders - const isExpanded = expandedDays.has(day.date) return (
- {/* Основная строка дня - как в Figma */} -
toggleDay(day.date)} - > + {/* Основная строка дня */} +
- {isExpanded ? : } - {day.date} + {formatDate(day.date)}
{formatCurrency(dayTotalCost)}
{dayOrders}
-
- {showWbAds ? formatCurrency(dayWbCost) : '—'} -
-
- {showExternalAds ? formatCurrency(dayExternalCost) : '—'} -
+
{formatCurrency(dayWbCost)}
+
{formatCurrency(dayExternalCost)}
- {/* Раскрывающееся содержимое с товарами */} - {isExpanded && ( -
- {day.products.map((product) => { + {/* Товары всегда видны - второй уровень */} +
+ {day.products.map((product) => { const productKey = `${day.date}-${product.nmId}` - const isProductExpanded = expandedProducts.has(productKey) const productExternalCost = product.advertising.externalAds.reduce((sum, ad) => sum + ad.cost, 0) + const productTotalCost = product.totalCost + productExternalCost return ( -
- {/* Строка товара */} -
toggleProduct(day.date, product.nmId)} - > -
- {isProductExpanded ? : } -
- {productPhotos.has(product.nmId) && ( - {product.name} - )} -
-
{product.name}
-
#{product.nmId}
-
+
+ {/* Строка товара с многострочными ячейками */} +
+ {/* Карточка товара */} +
+ {productPhotos.has(product.nmId) && ( + {product.name} + )} +
+
{product.name}
+
#{product.nmId}
-
{formatCurrency(product.totalCost + productExternalCost)}
-
{product.totalOrders}
-
- {showWbAds ? formatCurrency(product.totalCost) : '—'} -
-
- {showExternalAds ? formatCurrency(productExternalCost) : '—'} -
-
- {/* Раскрывающееся содержимое товара с кампаниями */} - {isProductExpanded && ( -
- {/* ВБ кампании */} - {showWbAds && product.advertising.wbCampaigns.map((campaign) => ( -
-
- - ВБ #{campaign.campaignId} -
-
{formatCurrency(campaign.cost)}
-
{campaign.orders}
-
{formatCurrency(campaign.cost)}
-
+ {/* Общая сумма */} +
{formatCurrency(productTotalCost)}
+ + {/* Заказы */} +
{product.totalOrders}
+ + {/* Реклама ВБ - многострочная ячейка */} +
+ {product.advertising.wbCampaigns.length > 0 ? ( +
+ {product.advertising.wbCampaigns.map((campaign, index) => ( +
+
+ {index === 0 ? 'Авто' : index === 1 ? 'Фразы' : index === 2 ? 'Предмет' : `Тип ${campaign.campaignId}`} +
+
{formatCurrency(campaign.cost)}
+
{campaign.orders} зак.
+
+ ))}
- ))} + ) : ( + + )} +
- {/* Внешняя реклама */} - {showExternalAds && product.advertising.externalAds.map((ad) => ( -
-
- - {ad.name} -
-
{formatCurrency(ad.cost)}
-
-
-
- {formatCurrency(ad.cost)} -
+ {/* Реклама внешняя - многострочная ячейка с кнопками */} +
+
+ {product.advertising.externalAds.map((ad) => ( +
+
{ad.name}
+
{formatCurrency(ad.cost)}
+
{ad.clicks || 0} кликов
+
{onGenerateLink && ( )} {onRemoveExternalAd && ( @@ -306,22 +376,21 @@ export function SimpleAdvertisingTable({ e.stopPropagation() 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="Удалить" > - + )}
-
- ))} - - {/* Кнопка добавления внешней рекламы */} - {onAddExternalAd && ( -
-
+ ))} + + {/* Инлайн форма добавления внешней рекламы */} + {onAddExternalAd && ( +
{showAddForm === productKey ? ( -
+
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" /> - - +
+ + +
) : ( - + + Добавить + )}
-
- )} + )} +
- )} +
) })} -
- )} + + {/* Кнопка добавления рекламы для нового товара */} + {onAddExternalAd && ( +
+ {showProductList === day.date ? ( +
+
+

Выберите товар для рекламы

+ +
+ + {/* Поиск среди загруженных товаров */} +
+ + { + setSearchTerm(e.target.value) + filterProducts(e.target.value) + }} + className="bg-white/10 border-white/20 text-white text-sm" + /> +
+ + {warehouseLoading ? ( +
+ Загрузка товаров... +
+ ) : warehouseError ? ( +
+ Ошибка загрузки товаров: {warehouseError.message} +
+ ) : filteredProducts.length > 0 ? ( +
+ {filteredProducts.map((product) => ( +
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) && ( + {product.title} + )} +
+
+ {product.title} +
+
+
{product.brand} • #{product.nmId}
+
Артикул: {product.vendorCode}
+
На складе: {product.totalQuantity} шт.
+
+
+
+ ))} +
+ ) : ( +
+ {searchTerm ? 'Товары не найдены' : 'Нет доступных товаров'} +
+ )} +
+ ) : showAddForm?.startsWith(`new-product-${day.date}`) ? ( +
+ {selectedProduct && ( +
+ {getProductImage(selectedProduct) && ( + {selectedProduct.title} + )} +
+
+ {selectedProduct.title} +
+
+
{selectedProduct.brand} • #{selectedProduct.nmId}
+
Артикул: {selectedProduct.vendorCode}
+
+
+
+ )} + + setNewExternalAd(prev => ({ ...prev, name: e.target.value }))} + className="bg-white/10 border-white/20 text-white text-sm" + /> + setNewExternalAd(prev => ({ ...prev, url: e.target.value }))} + className="bg-white/10 border-white/20 text-white text-sm" + /> + setNewExternalAd(prev => ({ ...prev, cost: e.target.value }))} + className="bg-white/10 border-white/20 text-white text-sm" + /> + +
+ + +
+
+ ) : ( + + )} +
+ )} +
) })}