Добавлены новые зависимости для работы с графиками и статистикой: интегрирован пакет recharts для визуализации данных. Обновлены компоненты бизнес-демо и сайдбара, добавлены новые функции для отображения информации о поставках и статистике. Улучшена структура кода и взаимодействие с пользователем. Обновлены GraphQL резолверы для получения статистики Wildberries.

This commit is contained in:
Bivekich
2025-07-22 13:29:15 +03:00
parent 20c27a2fa2
commit a62a09faca
13 changed files with 2388 additions and 122 deletions

View File

@ -0,0 +1,525 @@
"use client"
import { useState, useEffect } from 'react'
import { useQuery } from '@apollo/client'
import { gql } from '@apollo/client'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Skeleton } from '@/components/ui/skeleton'
import { TrendingUp } from 'lucide-react'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
ResponsiveContainer,
} from 'recharts'
// GraphQL query для получения статистики WB
const GET_WILDBERRIES_STATISTICS = gql`
query GetWildberriesStatistics(
$period: String
$startDate: String
$endDate: String
) {
getWildberriesStatistics(
period: $period
startDate: $startDate
endDate: $endDate
) {
success
message
data {
date
sales
orders
advertising
refusals
returns
revenue
buyoutPercentage
}
}
}
`
interface SalesTabProps {
selectedPeriod: string
useCustomDates?: boolean
startDate?: string
endDate?: string
}
// Mock данные для графиков
const mockChartData = [
{ date: '01.11', sales: 45, orders: 52, advertising: 32, refusals: 8, returns: 3 },
{ date: '02.11', sales: 35, orders: 41, advertising: 28, refusals: 6, returns: 2 },
{ date: '03.11', sales: 52, orders: 61, advertising: 39, refusals: 9, returns: 4 },
{ date: '04.11', sales: 38, orders: 45, advertising: 31, refusals: 7, returns: 2 },
{ date: '05.11', sales: 58, orders: 69, advertising: 45, refusals: 11, returns: 5 },
{ date: '06.11', sales: 47, orders: 55, advertising: 37, refusals: 8, returns: 3 },
{ date: '07.11', sales: 56, orders: 66, advertising: 42, refusals: 10, returns: 4 },
]
// Конфигурация chart
const chartConfig = {
sales: {
label: "Продажи",
color: "#10b981", // зеленый
},
orders: {
label: "Заказы",
color: "#3b82f6", // синий
},
advertising: {
label: "Реклама",
color: "#f59e0b", // оранжевый
},
refusals: {
label: "Отказы",
color: "#ef4444", // красный
},
returns: {
label: "Возвраты",
color: "#8b5cf6", // фиолетовый
},
} satisfies ChartConfig
// Mock данные для таблицы
const mockTableData = [
{
date: '01.11.2024',
salesUnits: 45,
buyoutPercentage: 82.2,
advertising: 320,
orders: 52,
refusals: 8,
returns: 3,
revenue: 1250
},
{
date: '02.11.2024',
salesUnits: 35,
buyoutPercentage: 85.7,
advertising: 280,
orders: 41,
refusals: 6,
returns: 2,
revenue: 980
},
{
date: '03.11.2024',
salesUnits: 52,
buyoutPercentage: 78.8,
advertising: 390,
orders: 61,
refusals: 9,
returns: 4,
revenue: 1450
},
{
date: '04.11.2024',
salesUnits: 38,
buyoutPercentage: 84.4,
advertising: 310,
orders: 45,
refusals: 7,
returns: 2,
revenue: 1120
},
{
date: '05.11.2024',
salesUnits: 58,
buyoutPercentage: 80.6,
advertising: 450,
orders: 69,
refusals: 11,
returns: 5,
revenue: 1680
},
{
date: '06.11.2024',
salesUnits: 47,
buyoutPercentage: 83.0,
advertising: 370,
orders: 55,
refusals: 8,
returns: 3,
revenue: 1350
},
{
date: '07.11.2024',
salesUnits: 56,
buyoutPercentage: 81.2,
advertising: 420,
orders: 66,
refusals: 10,
returns: 4,
revenue: 1580
},
]
export function SalesTab({ selectedPeriod, useCustomDates, startDate, endDate }: SalesTabProps) {
// Состояния для чекбоксов фильтрации
const [visibleMetrics, setVisibleMetrics] = useState({
sales: true,
orders: true,
advertising: true,
refusals: true,
returns: true,
})
// Получаем данные из WB API
const { data: wbData, loading, error } = useQuery(GET_WILDBERRIES_STATISTICS, {
variables: useCustomDates
? { startDate, endDate }
: { period: selectedPeriod },
errorPolicy: 'all',
skip: useCustomDates && (!startDate || !endDate) // Не запрашиваем пока не выбраны обе даты
})
// Данные для графика и таблицы
const [chartData, setChartData] = useState<typeof mockChartData>([])
const [tableData, setTableData] = useState<typeof mockTableData>([])
useEffect(() => {
if (wbData?.getWildberriesStatistics?.success && wbData.getWildberriesStatistics.data) {
const realData = wbData.getWildberriesStatistics.data
// Обновляем данные для графика
const newChartData = realData.map((item: {
date: string;
sales: number;
orders: number;
advertising: number;
refusals: number;
returns: number;
}) => ({
date: new Date(item.date).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' }),
sales: item.sales,
orders: item.orders,
advertising: Math.round(item.advertising),
refusals: item.refusals,
returns: item.returns
}))
// Обновляем данные для таблицы
const newTableData = realData.map((item: {
date: string;
sales: number;
orders: number;
advertising: number;
refusals: number;
returns: number;
revenue: number;
buyoutPercentage: number;
}) => ({
date: new Date(item.date).toLocaleDateString('ru-RU'),
salesUnits: item.sales,
buyoutPercentage: item.buyoutPercentage,
advertising: Math.round(item.advertising),
orders: item.orders,
refusals: item.refusals,
returns: item.returns,
revenue: Math.round(item.revenue)
}))
setChartData(newChartData)
setTableData(newTableData)
}
}, [wbData])
// Функция для переключения видимости метрики
const toggleMetric = (metric: keyof typeof visibleMetrics) => {
setVisibleMetrics(prev => ({
...prev,
[metric]: !prev[metric]
}))
}
// Проверяем состояние загрузки и данных
const isLoading = loading || (useCustomDates && (!startDate || !endDate))
const hasData = tableData.length > 0
const hasAnyActivity = hasData && tableData.some(row =>
row.salesUnits > 0 || row.orders > 0 || row.advertising > 0 || row.refusals > 0 || row.returns > 0
)
// Вычисляем итоги на основе текущих данных
const totals = {
salesUnits: tableData.reduce((sum: number, row: { salesUnits: number }) => sum + row.salesUnits, 0),
buyoutPercentage: tableData.length > 0 ? tableData.reduce((sum: number, row: { buyoutPercentage: number }) => sum + row.buyoutPercentage, 0) / tableData.length : 0,
advertising: tableData.reduce((sum: number, row: { advertising: number }) => sum + row.advertising, 0),
orders: tableData.reduce((sum: number, row: { orders: number }) => sum + row.orders, 0),
refusals: tableData.reduce((sum: number, row: { refusals: number }) => sum + row.refusals, 0),
returns: tableData.reduce((sum: number, row: { returns: number }) => sum + row.returns, 0),
revenue: tableData.reduce((sum: number, row: { revenue: number }) => sum + row.revenue, 0),
}
// Если загружается
if (isLoading) {
return (
<div className="h-full flex flex-col space-y-3">
<Card className="glass-card p-4 flex-shrink-0" style={{ height: '380px' }}>
<div className="h-full flex flex-col">
<Skeleton className="h-6 w-48 mb-4" />
<div className="flex flex-wrap gap-2 mb-4">
{[1,2,3,4,5].map(i => (
<Skeleton key={i} className="h-8 w-20" />
))}
</div>
<Skeleton className="flex-1 w-full" />
</div>
</Card>
<Card className="glass-card p-4 flex-1">
<Skeleton className="h-6 w-40 mb-4" />
<div className="space-y-2">
{[1,2,3,4,5].map(i => (
<Skeleton key={i} className="h-8 w-full" />
))}
</div>
</Card>
</div>
)
}
// Если нет данных или активности
if (!hasData || !hasAnyActivity) {
return (
<div className="h-full flex items-center justify-center">
<Card className="glass-card p-8 text-center max-w-md">
<div className="mb-4 flex justify-center">
<TrendingUp className="h-16 w-16 text-white/30" />
</div>
<h3 className="text-xl font-semibold text-white mb-2">
{!hasData ? 'Нет данных' : 'Нет активности'}
</h3>
<p className="text-white/60 text-sm">
{!hasData
? 'За выбранный период данные отсутствуют'
: 'За выбранный период не было продаж, заказов или рекламной активности'
}
</p>
{error && (
<div className="mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
<p className="text-red-400 text-xs">Ошибка: {error.message}</p>
</div>
)}
</Card>
</div>
)
}
return (
<div className="h-full flex flex-col space-y-3">
{/* График с фильтрами */}
<Card className="glass-card p-4 flex-shrink-0 overflow-hidden" style={{ height: '380px' }}>
<div className="h-full flex flex-col min-h-0">
{/* Компактный заголовок */}
<div className="flex items-center justify-between mb-2">
<h3 className="text-white font-semibold">Динамика показателей</h3>
{error && (
<div className="text-red-400 text-xs">Предупреждение: {error.message}</div>
)}
</div>
{/* Компактные чекбоксы для фильтрации */}
<div className="mb-2 pb-2 border-b border-white/10">
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{Object.entries(chartConfig).map(([key, config]) => {
const isVisible = visibleMetrics[key as keyof typeof visibleMetrics]
return (
<div
key={key}
className={`flex items-center space-x-1.5 p-1.5 rounded-lg transition-all duration-200 cursor-pointer ${
isVisible
? 'bg-white/10 border border-white/20 shadow-sm'
: 'bg-white/5 border border-white/10 hover:bg-white/8'
}`}
onClick={() => toggleMetric(key as keyof typeof visibleMetrics)}
>
<Checkbox
id={key}
checked={isVisible}
onCheckedChange={() => toggleMetric(key as keyof typeof visibleMetrics)}
className="border-white/30 data-[state=checked]:bg-white/20 data-[state=checked]:border-white/50"
/>
<label
htmlFor={key}
className={`text-xs cursor-pointer select-none flex items-center gap-1.5 transition-colors duration-200 ${
isVisible ? 'text-white font-medium' : 'text-white/60'
}`}
>
<div
className={`w-2.5 h-2.5 rounded-sm transition-all duration-200 ${
isVisible ? 'opacity-100 scale-100' : 'opacity-50 scale-90'
}`}
style={{ backgroundColor: config.color }}
/>
{config.label}
</label>
</div>
)
})}
</div>
</div>
<div className="flex-1 min-h-0">
<ChartContainer config={chartConfig} className="min-h-[200px] h-full w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 5)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dashed" />}
/>
{/* Условно рендерим бары в зависимости от чекбоксов */}
{visibleMetrics.sales && (
<Bar
dataKey="sales"
fill={chartConfig.sales.color}
radius={4}
/>
)}
{visibleMetrics.orders && (
<Bar
dataKey="orders"
fill={chartConfig.orders.color}
radius={4}
/>
)}
{visibleMetrics.advertising && (
<Bar
dataKey="advertising"
fill={chartConfig.advertising.color}
radius={4}
/>
)}
{visibleMetrics.refusals && (
<Bar
dataKey="refusals"
fill={chartConfig.refusals.color}
radius={4}
/>
)}
{visibleMetrics.returns && (
<Bar
dataKey="returns"
fill={chartConfig.returns.color}
radius={4}
/>
)}
</BarChart>
</ChartContainer>
</div>
</div>
</Card>
{/* Таблица данных */}
<Card className="glass-card flex-1 overflow-hidden">
<div className="p-4 h-full flex flex-col">
<h3 className="text-white font-semibold mb-3 text-sm">Детальная статистика</h3>
<div className="overflow-x-auto flex-1">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-2 text-white font-semibold text-xs">Дата</th>
<th className="text-left p-2 text-white font-semibold text-xs">Продажи, шт</th>
<th className="text-left p-2 text-white font-semibold text-xs">% выкупов</th>
<th className="text-left p-2 text-white font-semibold text-xs">Реклама, </th>
<th className="text-left p-2 text-white font-semibold text-xs">Заказы</th>
<th className="text-left p-2 text-white font-semibold text-xs">Отказы</th>
<th className="text-left p-2 text-white font-semibold text-xs">Возвраты</th>
<th className="text-left p-2 text-white font-semibold text-xs">Выручка, </th>
</tr>
</thead>
<tbody>
{tableData.map((row, index) => (
<tr key={index} className="border-b border-white/10 hover:bg-white/5 transition-colors">
<td className="p-2 text-white/80 text-xs">{row.date}</td>
<td className="p-2 text-white text-xs font-medium">{row.salesUnits}</td>
<td className="p-2 text-xs">
<Badge
variant="secondary"
className={`${
row.buyoutPercentage >= 80
? 'bg-green-500/20 text-green-400'
: 'bg-yellow-500/20 text-yellow-400'
}`}
>
{row.buyoutPercentage}%
</Badge>
</td>
<td className="p-2 text-white/80 text-xs">{row.advertising.toLocaleString('ru-RU')}</td>
<td className="p-2 text-white/80 text-xs">{row.orders}</td>
<td className="p-2 text-xs">
<Badge variant="secondary" className="bg-red-500/20 text-red-400 text-xs px-2 py-0.5">
{row.refusals}
</Badge>
</td>
<td className="p-2 text-xs">
<Badge variant="secondary" className="bg-orange-500/20 text-orange-400 text-xs px-2 py-0.5">
{row.returns}
</Badge>
</td>
<td className="p-2 text-white text-xs font-medium">
{row.revenue.toLocaleString('ru-RU')}
</td>
</tr>
))}
{/* Результирующая строка */}
<tr className="border-t-2 border-white/30 bg-white/10 font-semibold">
<td className="p-2 text-white text-xs font-bold">ИТОГО</td>
<td className="p-2 text-white text-xs font-bold">{totals.salesUnits}</td>
<td className="p-2 text-xs">
<Badge
variant="secondary"
className={`${
totals.buyoutPercentage >= 80
? 'bg-green-500/20 text-green-400'
: 'bg-yellow-500/20 text-yellow-400'
} font-bold`}
>
{totals.buyoutPercentage.toFixed(1)}%
</Badge>
</td>
<td className="p-2 text-white text-xs font-bold">{totals.advertising.toLocaleString('ru-RU')}</td>
<td className="p-2 text-white text-xs font-bold">{totals.orders}</td>
<td className="p-2 text-xs">
<Badge variant="secondary" className="bg-red-500/20 text-red-400 font-bold text-xs px-2 py-0.5">
{totals.refusals}
</Badge>
</td>
<td className="p-2 text-xs">
<Badge variant="secondary" className="bg-orange-500/20 text-orange-400 font-bold text-xs px-2 py-0.5">
{totals.returns}
</Badge>
</td>
<td className="p-2 text-white text-xs font-bold">
{totals.revenue.toLocaleString('ru-RU')}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</Card>
</div>
)
}

View File

@ -0,0 +1,168 @@
"use client"
import { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Card } from '@/components/ui/card'
import { Sidebar } from '@/components/dashboard/sidebar'
import { useSidebar } from '@/hooks/useSidebar'
import { SalesTab } from '@/components/seller-statistics/sales-tab'
import { DateRangePicker } from '@/components/ui/date-picker'
import { BarChart3, PieChart, TrendingUp, Calendar } from 'lucide-react'
export function SellerStatisticsDashboard() {
const { getSidebarMargin } = useSidebar()
const [selectedPeriod, setSelectedPeriod] = useState('week')
const [useCustomDates, setUseCustomDates] = useState(false)
const [startDate, setStartDate] = useState('')
const [endDate, setEndDate] = useState('')
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 overflow-hidden transition-all duration-300`}>
<div className="h-full w-full flex flex-col">
{/* Компактный заголовок с переключателями */}
<div className="flex items-center justify-between mb-3 flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Статистика продаж</h1>
<p className="text-white/50 text-sm">Аналитика продаж, заказов и рекламы</p>
</div>
{/* Переключатели периода и пользовательские даты */}
<div className="flex items-center gap-3">
{/* Стильные переключатели периода */}
<div className="flex gap-1 bg-white/5 backdrop-blur border border-white/10 rounded-xl p-1">
<button
onClick={() => {
setSelectedPeriod('week')
setUseCustomDates(false)
}}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 ${
selectedPeriod === 'week' && !useCustomDates
? 'bg-white/20 text-white shadow-sm'
: 'text-white/60 hover:bg-white/10 hover:text-white/80'
}`}
>
Неделя
</button>
<button
onClick={() => {
setSelectedPeriod('month')
setUseCustomDates(false)
}}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 ${
selectedPeriod === 'month' && !useCustomDates
? 'bg-white/20 text-white shadow-sm'
: 'text-white/60 hover:bg-white/10 hover:text-white/80'
}`}
>
Месяц
</button>
<button
onClick={() => {
setSelectedPeriod('quarter')
setUseCustomDates(false)
}}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 ${
selectedPeriod === 'quarter' && !useCustomDates
? 'bg-white/20 text-white shadow-sm'
: 'text-white/60 hover:bg-white/10 hover:text-white/80'
}`}
>
Квартал
</button>
<button
onClick={() => setUseCustomDates(true)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 flex items-center gap-1 ${
useCustomDates
? 'bg-white/20 text-white shadow-sm'
: 'text-white/60 hover:bg-white/10 hover:text-white/80'
}`}
>
<Calendar className="h-3 w-3" />
Период
</button>
</div>
{/* Выбор произвольных дат */}
{useCustomDates && (
<DateRangePicker
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
className="min-w-[280px]"
/>
)}
</div>
</div>
{/* Основной контент с табами */}
<div className="flex-1 overflow-hidden">
<Tabs defaultValue="sales" className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-3 bg-white/5 backdrop-blur border border-white/10 rounded-xl flex-shrink-0 h-11">
<TabsTrigger
value="sales"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 flex items-center gap-2 text-sm rounded-lg"
>
<BarChart3 className="h-4 w-4" />
Продажи
</TabsTrigger>
<TabsTrigger
value="advertising"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 flex items-center gap-2 text-sm rounded-lg"
>
<TrendingUp className="h-4 w-4" />
Реклама
</TabsTrigger>
<TabsTrigger
value="other"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 flex items-center gap-2 text-sm rounded-lg"
>
<PieChart className="h-4 w-4" />
Иное
</TabsTrigger>
</TabsList>
{/* Контент вкладок */}
<div className="flex-1 overflow-hidden mt-3">
<TabsContent value="sales" className="h-full m-0 overflow-hidden">
<SalesTab
selectedPeriod={selectedPeriod}
useCustomDates={useCustomDates}
startDate={startDate}
endDate={endDate}
/>
</TabsContent>
<TabsContent value="advertising" className="h-full m-0 overflow-hidden">
<Card className="glass-card h-full overflow-hidden p-6">
<div className="flex items-center justify-center h-full">
<div className="text-center">
<TrendingUp className="h-12 w-12 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">Статистика рекламы</h3>
<p className="text-white/60">Раздел в разработке</p>
</div>
</div>
</Card>
</TabsContent>
<TabsContent value="other" className="h-full m-0 overflow-hidden">
<Card className="glass-card h-full overflow-hidden p-6">
<div className="flex items-center justify-center h-full">
<div className="text-center">
<PieChart className="h-12 w-12 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">Прочая статистика</h3>
<p className="text-white/60">Раздел в разработке</p>
</div>
</div>
</Card>
</TabsContent>
</div>
</Tabs>
</div>
</div>
</main>
</div>
)
}