Обновление компонентов интерфейса и оптимизация логики
- Добавлен компонент AppShell в RootLayout для улучшения структуры - Обновлен компонент Sidebar для предотвращения дублирования при рендеринге - Оптимизированы импорты в компонентах AdvertisingTab и SalesTab - Реализована логика кэширования статистики селлера в GraphQL резолверах
This commit is contained in:
@ -9,7 +9,7 @@ import { MarketplaceService } from '@/services/marketplace-service'
|
||||
import { SmsService } from '@/services/sms-service'
|
||||
import { WildberriesService } from '@/services/wildberries-service'
|
||||
|
||||
import '@/lib/seed-init' // Автоматическая инициализация БД
|
||||
import '@/lib/seed-init'; // Автоматическая инициализация БД
|
||||
|
||||
// Сервисы
|
||||
const smsService = new SmsService()
|
||||
@ -7489,6 +7489,68 @@ const wildberriesQueries = {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching WB statistics:', error)
|
||||
// Фолбэк: пробуем вернуть последние данные из кеша статистики селлера
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: context.user!.id },
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
if (user?.organization) {
|
||||
const whereCache: any = {
|
||||
organizationId: user.organization.id,
|
||||
period: startDate && endDate ? 'custom' : period ?? 'week',
|
||||
}
|
||||
if (startDate && endDate) {
|
||||
whereCache.dateFrom = new Date(startDate)
|
||||
whereCache.dateTo = new Date(endDate)
|
||||
}
|
||||
|
||||
const cache = await prisma.sellerStatsCache.findFirst({
|
||||
where: whereCache,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
if (cache?.productsData) {
|
||||
// Ожидаем, что productsData — строка JSON с полями, сохраненными клиентом
|
||||
const parsed = JSON.parse(cache.productsData as unknown as string) as {
|
||||
tableData?: Array<{
|
||||
date: string
|
||||
salesUnits: number
|
||||
orders: number
|
||||
advertising: number
|
||||
refusals: number
|
||||
returns: number
|
||||
revenue: number
|
||||
buyoutPercentage: number
|
||||
}>
|
||||
}
|
||||
|
||||
const table = parsed.tableData ?? []
|
||||
const dataFromCache = table.map((row) => ({
|
||||
date: row.date,
|
||||
sales: row.salesUnits,
|
||||
orders: row.orders,
|
||||
advertising: row.advertising,
|
||||
refusals: row.refusals,
|
||||
returns: row.returns,
|
||||
revenue: row.revenue,
|
||||
buyoutPercentage: row.buyoutPercentage,
|
||||
}))
|
||||
|
||||
if (dataFromCache.length > 0) {
|
||||
return {
|
||||
success: true,
|
||||
data: dataFromCache,
|
||||
message: 'Данные возвращены из кеша из-за ошибки WB API',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (fallbackErr) {
|
||||
console.error('Seller stats cache fallback failed:', fallbackErr)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Ошибка получения статистики',
|
||||
@ -8186,6 +8248,84 @@ resolvers.Query = {
|
||||
...wildberriesQueries,
|
||||
...externalAdQueries,
|
||||
...wbWarehouseCacheQueries,
|
||||
// Кеш статистики селлера
|
||||
getSellerStatsCache: async (
|
||||
_: unknown,
|
||||
args: { period: string; dateFrom?: string | null; dateTo?: string | null },
|
||||
context: Context,
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
if (!user?.organization) {
|
||||
throw new GraphQLError('Организация не найдена')
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
// Для custom учитываем диапазон, иначе только period
|
||||
const where: any = {
|
||||
organizationId: user.organization.id,
|
||||
cacheDate: today,
|
||||
period: args.period,
|
||||
}
|
||||
if (args.period === 'custom') {
|
||||
if (!args.dateFrom || !args.dateTo) {
|
||||
throw new GraphQLError('Для custom необходимо указать dateFrom и dateTo')
|
||||
}
|
||||
where.dateFrom = new Date(args.dateFrom)
|
||||
where.dateTo = new Date(args.dateTo)
|
||||
}
|
||||
|
||||
const cache = await prisma.sellerStatsCache.findFirst({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
if (!cache) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Кеш не найден',
|
||||
cache: null,
|
||||
fromCache: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Данные получены из кеша',
|
||||
cache: {
|
||||
...cache,
|
||||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||||
dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null,
|
||||
dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null,
|
||||
productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null,
|
||||
advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null,
|
||||
createdAt: cache.createdAt.toISOString(),
|
||||
updatedAt: cache.updatedAt.toISOString(),
|
||||
},
|
||||
fromCache: true,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Seller Stats cache:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Ошибка получения кеша статистики',
|
||||
cache: null,
|
||||
fromCache: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
resolvers.Mutation = {
|
||||
@ -8193,4 +8333,87 @@ resolvers.Mutation = {
|
||||
...adminMutations,
|
||||
...externalAdMutations,
|
||||
...wbWarehouseCacheMutations,
|
||||
// Сохранение кеша статистики селлера
|
||||
saveSellerStatsCache: async (
|
||||
_: unknown,
|
||||
{ input }: { input: { period: string; dateFrom?: string | null; dateTo?: string | null; productsData?: string | null; productsTotalSales?: number | null; productsTotalOrders?: number | null; productsCount?: number | null; advertisingData?: string | null; advertisingTotalCost?: number | null; advertisingTotalViews?: number | null; advertisingTotalClicks?: number | null; expiresAt: string } },
|
||||
context: Context,
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
if (!user?.organization) {
|
||||
throw new GraphQLError('Организация не найдена')
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
const data: any = {
|
||||
organizationId: user.organization.id,
|
||||
cacheDate: today,
|
||||
period: input.period,
|
||||
dateFrom: input.period === 'custom' && input.dateFrom ? new Date(input.dateFrom) : null,
|
||||
dateTo: input.period === 'custom' && input.dateTo ? new Date(input.dateTo) : null,
|
||||
productsData: input.productsData ?? null,
|
||||
productsTotalSales: input.productsTotalSales ?? null,
|
||||
productsTotalOrders: input.productsTotalOrders ?? null,
|
||||
productsCount: input.productsCount ?? null,
|
||||
advertisingData: input.advertisingData ?? null,
|
||||
advertisingTotalCost: input.advertisingTotalCost ?? null,
|
||||
advertisingTotalViews: input.advertisingTotalViews ?? null,
|
||||
advertisingTotalClicks: input.advertisingTotalClicks ?? null,
|
||||
expiresAt: new Date(input.expiresAt),
|
||||
}
|
||||
|
||||
// upsert с составным уникальным ключом, содержащим NULL, в Prisma вызывает валидацию.
|
||||
// Делаем вручную: findFirst по уникальному набору, затем update или create.
|
||||
const existing = await prisma.sellerStatsCache.findFirst({
|
||||
where: {
|
||||
organizationId: user.organization.id,
|
||||
cacheDate: today,
|
||||
period: input.period,
|
||||
dateFrom: data.dateFrom,
|
||||
dateTo: data.dateTo,
|
||||
},
|
||||
})
|
||||
|
||||
const cache = existing
|
||||
? await prisma.sellerStatsCache.update({ where: { id: existing.id }, data })
|
||||
: await prisma.sellerStatsCache.create({ data })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Кеш статистики сохранен',
|
||||
cache: {
|
||||
...cache,
|
||||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||||
dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null,
|
||||
dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null,
|
||||
productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null,
|
||||
advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null,
|
||||
createdAt: cache.createdAt.toISOString(),
|
||||
updatedAt: cache.updatedAt.toISOString(),
|
||||
},
|
||||
fromCache: false,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving Seller Stats cache:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Ошибка сохранения кеша статистики',
|
||||
cache: null,
|
||||
fromCache: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
Reference in New Issue
Block a user