Добавлены новые зависимости для работы с графиками и статистикой, включая @radix-ui/react-popover, date-fns и react-day-picker. Обновлены компоненты для отображения статистики продаж, улучшена агрегация данных и добавлены функции сортировки в таблицах. Обновлены API маршруты для получения данных о статистике Wildberries. Оптимизирован код для повышения читаемости и производительности.

This commit is contained in:
Bivekich
2025-07-22 14:47:44 +03:00
parent a62a09faca
commit 20c4b665a1
15 changed files with 1688 additions and 486 deletions

View File

@ -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 }