Добавлены новые зависимости для работы с графиками и статистикой, включая @radix-ui/react-popover, date-fns и react-day-picker. Обновлены компоненты для отображения статистики продаж, улучшена агрегация данных и добавлены функции сортировки в таблицах. Обновлены API маршруты для получения данных о статистике Wildberries. Оптимизирован код для повышения читаемости и производительности.
This commit is contained in:
@ -127,6 +127,24 @@ export interface WBAdvertStatsRequest {
|
||||
}
|
||||
}
|
||||
|
||||
// Интерфейсы для API /adv/v1/promotion/count
|
||||
export interface WBCampaignListItem {
|
||||
advertId: number
|
||||
changeTime: string // date-time
|
||||
}
|
||||
|
||||
export interface WBAdvertGroup {
|
||||
type: number
|
||||
status: number
|
||||
count: number
|
||||
advert_list: WBCampaignListItem[]
|
||||
}
|
||||
|
||||
export interface WBCampaignsListResponse {
|
||||
adverts: WBAdvertGroup[] | null
|
||||
all: number
|
||||
}
|
||||
|
||||
export interface WBAdvertStatsResponse {
|
||||
dates: string[]
|
||||
views: number
|
||||
@ -152,9 +170,45 @@ export interface WBAdvertStatsResponse {
|
||||
shks: number
|
||||
sum_price: number
|
||||
}>
|
||||
boosterStats: Array<{
|
||||
date: string
|
||||
views: number
|
||||
clicks: number
|
||||
ctr: number
|
||||
cpc: number
|
||||
sum: number
|
||||
atbs: number
|
||||
orders: number
|
||||
cr: number
|
||||
shks: number
|
||||
sum_price: number
|
||||
}>
|
||||
advertId: number
|
||||
}
|
||||
|
||||
// Новые интерфейсы для метода getCampaignStats
|
||||
export interface WBCampaignStatsRequestWithDate {
|
||||
id: number
|
||||
dates: string[]
|
||||
}
|
||||
|
||||
export interface WBCampaignStatsRequestWithInterval {
|
||||
id: number
|
||||
interval: {
|
||||
begin: string
|
||||
end: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface WBCampaignStatsRequestWithCampaignID {
|
||||
id: number
|
||||
}
|
||||
|
||||
export type WBCampaignStatsRequest =
|
||||
| WBCampaignStatsRequestWithDate
|
||||
| WBCampaignStatsRequestWithInterval
|
||||
| WBCampaignStatsRequestWithCampaignID
|
||||
|
||||
export interface WBStatisticsData {
|
||||
date: string
|
||||
sales: number
|
||||
@ -204,13 +258,145 @@ class WildberriesService {
|
||||
return this.makeRequest<WBOrdersData[]>(url)
|
||||
}
|
||||
|
||||
// Получение рекламных кампаний
|
||||
async getAdverts(status?: number, type?: number, limit = 100, offset = 0): Promise<WBAdvertData[]> {
|
||||
let url = `${this.advertURL}/adv/v0/adverts?limit=${limit}&offset=${offset}`
|
||||
if (status) url += `&status=${status}`
|
||||
if (type) url += `&type=${type}`
|
||||
// Получение списка всех кампаний с группировкой
|
||||
async getCampaignsList(): Promise<WBCampaignsListResponse> {
|
||||
const url = `${this.advertURL}/adv/v1/promotion/count`
|
||||
console.log(`WB API: Getting campaigns list from ${url}`)
|
||||
return this.makeRequest<WBCampaignsListResponse>(url)
|
||||
}
|
||||
|
||||
// Получение кампаний за определенный период по changeTime
|
||||
async getCampaignsForPeriod(dateFrom: string, dateTo: string): Promise<number[]> {
|
||||
const campaignsList = await this.getCampaignsList()
|
||||
const fromDate = new Date(dateFrom)
|
||||
const toDate = new Date(dateTo)
|
||||
|
||||
return this.makeRequest<WBAdvertData[]>(url)
|
||||
const campaignIds: number[] = []
|
||||
|
||||
if (campaignsList.adverts) {
|
||||
for (const advertGroup of campaignsList.adverts) {
|
||||
if (advertGroup.advert_list) {
|
||||
for (const campaign of advertGroup.advert_list) {
|
||||
const changeTime = new Date(campaign.changeTime)
|
||||
// Проверяем что кампания была изменена в нужном периоде
|
||||
if (changeTime >= fromDate && changeTime <= toDate) {
|
||||
campaignIds.push(campaign.advertId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`WB API: Found ${campaignIds.length} campaigns for period ${dateFrom} - ${dateTo}`)
|
||||
return campaignIds
|
||||
}
|
||||
|
||||
// Старый метод для совместимости (используем новый API)
|
||||
async getAdverts(status?: number, type?: number, limit = 100, offset = 0): Promise<WBAdvertData[]> {
|
||||
const campaignsList = await this.getCampaignsList()
|
||||
const campaigns: WBAdvertData[] = []
|
||||
|
||||
if (campaignsList.adverts) {
|
||||
for (const advertGroup of campaignsList.adverts) {
|
||||
// Фильтрация по статусу и типу если указаны
|
||||
if (status && advertGroup.status !== status) continue
|
||||
if (type && advertGroup.type !== type) continue
|
||||
|
||||
if (advertGroup.advert_list) {
|
||||
for (const campaign of advertGroup.advert_list) {
|
||||
campaigns.push({
|
||||
advertId: campaign.advertId,
|
||||
type: advertGroup.type,
|
||||
status: advertGroup.status,
|
||||
name: `Campaign ${campaign.advertId}`,
|
||||
endTime: campaign.changeTime,
|
||||
createTime: campaign.changeTime,
|
||||
changeTime: campaign.changeTime,
|
||||
startTime: campaign.changeTime, // Используем changeTime как заглушку
|
||||
dailyBudget: 0, // Неизвестно из этого API
|
||||
budget: 0 // Неизвестно из этого API
|
||||
})
|
||||
|
||||
// Применяем лимит
|
||||
if (campaigns.length >= limit) break
|
||||
}
|
||||
}
|
||||
if (campaigns.length >= limit) break
|
||||
}
|
||||
}
|
||||
|
||||
return campaigns.slice(offset, offset + limit)
|
||||
}
|
||||
|
||||
// Получение статистики конкретных кампаний (новый метод)
|
||||
async getCampaignStats(requests: WBCampaignStatsRequest[]): Promise<WBAdvertStatsResponse[]> {
|
||||
if (!requests || requests.length === 0) {
|
||||
throw new Error('Requests array cannot be empty')
|
||||
}
|
||||
|
||||
if (requests.length > 100) {
|
||||
throw new Error('Maximum 100 campaigns can be requested at once')
|
||||
}
|
||||
|
||||
const url = `${this.advertURL}/adv/v2/fullstats`
|
||||
|
||||
console.log(`WB API: Requesting campaign stats for ${requests.length} campaigns`)
|
||||
console.log(`WB API: Request body:`, JSON.stringify(requests, null, 2))
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest<WBAdvertStatsResponse[]>(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requests)
|
||||
})
|
||||
|
||||
console.log(`WB API: Campaign stats response:`, JSON.stringify(response, null, 2))
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error(`WB API: Campaign stats error:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Получение списка складов
|
||||
async getWarehouses(): Promise<WildberriesWarehouse[]> {
|
||||
const url = `${this.baseURL}/api/v2/warehouses`
|
||||
console.log(`WB API: Getting warehouses from ${url}`)
|
||||
const response = await this.makeRequest<WildberriesWarehouse[]>(url)
|
||||
return response || []
|
||||
}
|
||||
|
||||
// Получение карточек товаров
|
||||
async getCards(options: { limit?: number; offset?: number } = {}): Promise<WildberriesCard[]> {
|
||||
const { limit = 100, offset = 0 } = options
|
||||
const url = `${this.baseURL}/content/v1/cards/cursor/list?sort=updateAt&limit=${limit}&cursor=${offset}`
|
||||
console.log(`WB API: Getting cards from ${url}`)
|
||||
try {
|
||||
const response = await this.makeRequest<{ cards: WildberriesCard[] }>(url)
|
||||
return response?.cards || []
|
||||
} catch (error) {
|
||||
console.error(`WB API: Error getting cards:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Поиск карточек товаров
|
||||
async searchCards(searchTerm: string, limit = 100): Promise<WildberriesCard[]> {
|
||||
// Для простоты пока используем тот же API что и getCards
|
||||
// В реальности может потребоваться другой endpoint для поиска
|
||||
const cards = await this.getCards({ limit })
|
||||
|
||||
// Фильтруем результаты по поисковому запросу
|
||||
const filteredCards = cards.filter(card => {
|
||||
const searchLower = searchTerm.toLowerCase()
|
||||
return (
|
||||
card.vendorCode?.toLowerCase().includes(searchLower) ||
|
||||
card.object?.toLowerCase().includes(searchLower) ||
|
||||
card.brand?.toLowerCase().includes(searchLower)
|
||||
)
|
||||
})
|
||||
|
||||
console.log(`WB API: Search "${searchTerm}" found ${filteredCards.length} cards`)
|
||||
return filteredCards
|
||||
}
|
||||
|
||||
// Получение статистики всех рекламных кампаний (v2)
|
||||
@ -261,13 +447,37 @@ class WildberriesService {
|
||||
|
||||
console.log(`WB API: Got ${salesData.length} sales, ${ordersData.length} orders`)
|
||||
|
||||
// Получаем статистику рекламы напрямую через /adv/v2/fullstats
|
||||
let advertStatsData: WBAdvertStatsResponse[] = []
|
||||
// Получаем статистику рекламы через правильный API
|
||||
let advertStatsData: WBAdvertStatsResponse[] = []
|
||||
try {
|
||||
console.log(`WB API: Getting campaign stats for interval: ${dateFrom} to ${dateTo}`)
|
||||
|
||||
advertStatsData = await this.getAdvertStats(dateFrom, dateTo)
|
||||
console.log(`WB API: Got advertising stats for ${advertStatsData.length} campaigns`)
|
||||
console.log(`WB API: Getting campaign stats for interval: ${dateFrom} to ${dateTo}`)
|
||||
|
||||
try {
|
||||
// Получаем ID кампаний, которые были изменены в указанном периоде
|
||||
const campaignIds = await this.getCampaignsForPeriod(dateFrom, dateTo)
|
||||
|
||||
if (campaignIds.length > 0) {
|
||||
// Создаем запросы для /adv/v2/fullstats с интервалом дат
|
||||
const campaignRequests: WBCampaignStatsRequest[] = campaignIds.map(id => ({
|
||||
id,
|
||||
interval: {
|
||||
begin: dateFrom,
|
||||
end: dateTo
|
||||
}
|
||||
}))
|
||||
|
||||
// Получаем статистику кампаний
|
||||
advertStatsData = await this.getCampaignStats(campaignRequests)
|
||||
console.log(`WB API: Got advertising stats for ${advertStatsData.length} campaigns`)
|
||||
} else {
|
||||
console.log(`WB API: No campaigns found for the specified period`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`WB API: Failed to get campaign stats:`, error)
|
||||
console.log(`WB API: Skipping advertising stats due to API error`)
|
||||
}
|
||||
|
||||
console.log(`WB API: Got advertising stats for ${advertStatsData.length} campaigns total`)
|
||||
|
||||
// Логируем детали рекламных затрат
|
||||
advertStatsData.forEach(stat => {
|
||||
@ -282,13 +492,21 @@ class WildberriesService {
|
||||
const statsMap = new Map<string, WBStatisticsData>()
|
||||
|
||||
// Обрабатываем продажи
|
||||
console.log(`WB API: Processing ${salesData.length} sales records`)
|
||||
salesData.forEach(sale => {
|
||||
const originalDate = sale.date
|
||||
const date = sale.date.split('T')[0] // Берем только дату без времени
|
||||
|
||||
console.log(`WB API: Processing sale - original date: ${originalDate}, normalized: ${date}`)
|
||||
|
||||
// Строгая фильтрация по диапазону дат
|
||||
if (date < dateFrom || date > dateTo) return
|
||||
if (date < dateFrom || date > dateTo) {
|
||||
console.log(`WB API: Skipping sale ${date} - outside range ${dateFrom} to ${dateTo}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!statsMap.has(date)) {
|
||||
console.log(`WB API: Creating new stats entry for date ${date}`)
|
||||
statsMap.set(date, {
|
||||
date,
|
||||
sales: 0,
|
||||
@ -305,19 +523,29 @@ class WildberriesService {
|
||||
if (!sale.isCancel) {
|
||||
stats.sales += 1
|
||||
stats.revenue += sale.totalPrice * (1 - sale.discountPercent / 100)
|
||||
console.log(`WB API: Added sale to ${date}, total sales now: ${stats.sales}`)
|
||||
} else {
|
||||
stats.returns += 1
|
||||
console.log(`WB API: Added return to ${date}, total returns now: ${stats.returns}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Обрабатываем заказы
|
||||
console.log(`WB API: Processing ${ordersData.length} orders records`)
|
||||
ordersData.forEach(order => {
|
||||
const originalDate = order.date
|
||||
const date = order.date.split('T')[0]
|
||||
|
||||
console.log(`WB API: Processing order - original date: ${originalDate}, normalized: ${date}`)
|
||||
|
||||
// Строгая фильтрация по диапазону дат
|
||||
if (date < dateFrom || date > dateTo) return
|
||||
if (date < dateFrom || date > dateTo) {
|
||||
console.log(`WB API: Skipping order ${date} - outside range ${dateFrom} to ${dateTo}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!statsMap.has(date)) {
|
||||
console.log(`WB API: Creating new stats entry for date ${date} (from orders)`)
|
||||
statsMap.set(date, {
|
||||
date,
|
||||
sales: 0,
|
||||
@ -333,8 +561,10 @@ class WildberriesService {
|
||||
const stats = statsMap.get(date)!
|
||||
if (!order.isCancel) {
|
||||
stats.orders += 1
|
||||
console.log(`WB API: Added order to ${date}, total orders now: ${stats.orders}`)
|
||||
} else {
|
||||
stats.refusals += 1
|
||||
console.log(`WB API: Added refusal to ${date}, total refusals now: ${stats.refusals}`)
|
||||
}
|
||||
})
|
||||
|
||||
@ -390,7 +620,11 @@ class WildberriesService {
|
||||
// Получаем данные по рекламе (это более сложно, так как нужна отдельная статистика)
|
||||
// Пока используем заглушку, в реальности нужно вызвать отдельный API
|
||||
|
||||
return Array.from(statsMap.values()).sort((a, b) => a.date.localeCompare(b.date))
|
||||
const finalResults = Array.from(statsMap.values()).sort((a, b) => a.date.localeCompare(b.date))
|
||||
console.log(`WB API: Final aggregated results:`, finalResults)
|
||||
console.log(`WB API: Unique dates count: ${finalResults.length}`)
|
||||
|
||||
return finalResults
|
||||
} catch (error) {
|
||||
console.error('Error fetching WB statistics:', error)
|
||||
throw error
|
||||
@ -443,6 +677,24 @@ class WildberriesService {
|
||||
|
||||
return this.formatDate(date)
|
||||
}
|
||||
|
||||
// Статический метод для получения складов с токеном
|
||||
static async getWarehouses(apiKey: string): Promise<WildberriesWarehouse[]> {
|
||||
const service = new WildberriesService(apiKey)
|
||||
return service.getWarehouses()
|
||||
}
|
||||
|
||||
// Статический метод для получения карточек с токеном
|
||||
static async getAllCards(apiKey: string, limit = 100): Promise<WildberriesCard[]> {
|
||||
const service = new WildberriesService(apiKey)
|
||||
return service.getCards({ limit })
|
||||
}
|
||||
|
||||
// Статический метод для поиска карточек с токеном
|
||||
static async searchCards(apiKey: string, searchTerm: string, limit = 100): Promise<WildberriesCard[]> {
|
||||
const service = new WildberriesService(apiKey)
|
||||
return service.searchCards(searchTerm, limit)
|
||||
}
|
||||
}
|
||||
|
||||
export { WildberriesService }
|
Reference in New Issue
Block a user