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

This commit is contained in:
Veronika Smirnova
2025-07-26 17:21:58 +03:00
parent f786d2f8fe
commit 25fead48e9
3 changed files with 1618 additions and 402 deletions

View File

@ -17,6 +17,7 @@ import { InteractiveDemo } from "./ui-kit/interactive-demo";
import { BusinessDemo } from "./ui-kit/business-demo";
import { TimesheetDemo } from "./ui-kit/timesheet-demo";
import { FulfillmentWarehouseDemo } from "./ui-kit/fulfillment-warehouse-demo";
import { FulfillmentWarehouse2Demo } from "./ui-kit/fulfillment-warehouse-2-demo";
import { SuppliesDemo } from "./ui-kit/supplies-demo";
import { WBWarehouseDemo } from "./ui-kit/wb-warehouse-demo";
import { SuppliesNavigationDemo } from "./ui-kit/supplies-navigation-demo";
@ -109,7 +110,7 @@ export function UIKitSection() {
value="interactive"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 text-xs px-3 py-2"
>
Интерактив
Интерактивные
</TabsTrigger>
<TabsTrigger
value="business"
@ -129,6 +130,12 @@ export function UIKitSection() {
>
Склад фулфилмент
</TabsTrigger>
<TabsTrigger
value="fulfillment-warehouse-2"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 text-xs px-3 py-2"
>
Склад фулфилмент - 2
</TabsTrigger>
<TabsTrigger
value="supplies"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 text-xs px-3 py-2"
@ -213,6 +220,10 @@ export function UIKitSection() {
<FulfillmentWarehouseDemo />
</TabsContent>
<TabsContent value="fulfillment-warehouse-2" className="space-y-6">
<FulfillmentWarehouse2Demo />
</TabsContent>
<TabsContent value="supplies" className="space-y-6">
<SuppliesDemo />
</TabsContent>

View File

@ -0,0 +1,699 @@
"use client";
import { useState, useMemo } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
Package,
TrendingUp,
TrendingDown,
AlertTriangle,
RotateCcw,
Wrench,
Users,
ChevronDown,
ChevronUp,
Box,
Search,
ArrowUpDown,
Store,
Package2,
} from "lucide-react";
// Типы данных
interface StoreData {
id: string;
name: string;
logo?: string;
products: number;
goods: number;
defects: number;
sellerSupplies: number;
pvzReturns: number;
// Изменения за сутки
productsChange: number;
goodsChange: number;
defectsChange: number;
sellerSuppliesChange: number;
pvzReturnsChange: number;
}
interface WarehouseStats {
products: { current: number; change: number };
goods: { current: number; change: number };
defects: { current: number; change: number };
pvzReturns: { current: number; change: number };
fulfillmentSupplies: { current: number; change: number };
sellerSupplies: { current: number; change: number };
}
export function FulfillmentWarehouse2Demo() {
// Состояния для поиска и фильтрации
const [searchTerm, setSearchTerm] = useState("");
const [sortField, setSortField] = useState<keyof StoreData>("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set());
// Мок данные для статистики
const warehouseStats: WarehouseStats = {
products: { current: 2856, change: 124 },
goods: { current: 1391, change: 87 },
defects: { current: 43, change: -12 },
pvzReturns: { current: 256, change: 34 },
fulfillmentSupplies: { current: 189, change: 23 },
sellerSupplies: { current: 534, change: 67 },
};
// Мок данные для магазинов
const mockStoreData: StoreData[] = useMemo(
() => [
{
id: "1",
name: "Электроника Плюс",
products: 456,
goods: 234,
defects: 12,
sellerSupplies: 89,
pvzReturns: 45,
productsChange: 23,
goodsChange: 15,
defectsChange: -3,
sellerSuppliesChange: 12,
pvzReturnsChange: 8,
},
{
id: "2",
name: "Мода и Стиль",
products: 678,
goods: 345,
defects: 8,
sellerSupplies: 123,
pvzReturns: 67,
productsChange: 34,
goodsChange: 22,
defectsChange: -2,
sellerSuppliesChange: 18,
pvzReturnsChange: 12,
},
{
id: "3",
name: "Дом и Сад",
products: 289,
goods: 156,
defects: 5,
sellerSupplies: 67,
pvzReturns: 23,
productsChange: 12,
goodsChange: 8,
defectsChange: -1,
sellerSuppliesChange: 9,
pvzReturnsChange: 4,
},
{
id: "4",
name: "Спорт и Отдых",
products: 567,
goods: 289,
defects: 15,
sellerSupplies: 134,
pvzReturns: 78,
productsChange: 28,
goodsChange: 19,
defectsChange: -4,
sellerSuppliesChange: 21,
pvzReturnsChange: 15,
},
{
id: "5",
name: "Красота и Здоровье",
products: 234,
goods: 123,
defects: 3,
sellerSupplies: 45,
pvzReturns: 19,
productsChange: 8,
goodsChange: 5,
defectsChange: 0,
sellerSuppliesChange: 6,
pvzReturnsChange: 3,
},
],
[]
);
// Фильтрация и сортировка данных
const filteredAndSortedStores = useMemo(() => {
const filtered = mockStoreData.filter((store) =>
store.name.toLowerCase().includes(searchTerm.toLowerCase())
);
filtered.sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
if (typeof aValue === "string" && typeof bValue === "string") {
return sortOrder === "asc"
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}
if (typeof aValue === "number" && typeof bValue === "number") {
return sortOrder === "asc" ? aValue - bValue : bValue - aValue;
}
return 0;
});
return filtered;
}, [searchTerm, sortField, sortOrder, mockStoreData]);
// Подсчет общих сумм
const totals = useMemo(() => {
return filteredAndSortedStores.reduce(
(acc, store) => ({
products: acc.products + store.products,
goods: acc.goods + store.goods,
defects: acc.defects + store.defects,
sellerSupplies: acc.sellerSupplies + store.sellerSupplies,
pvzReturns: acc.pvzReturns + store.pvzReturns,
productsChange: acc.productsChange + store.productsChange,
goodsChange: acc.goodsChange + store.goodsChange,
defectsChange: acc.defectsChange + store.defectsChange,
sellerSuppliesChange:
acc.sellerSuppliesChange + store.sellerSuppliesChange,
pvzReturnsChange: acc.pvzReturnsChange + store.pvzReturnsChange,
}),
{
products: 0,
goods: 0,
defects: 0,
sellerSupplies: 0,
pvzReturns: 0,
productsChange: 0,
goodsChange: 0,
defectsChange: 0,
sellerSuppliesChange: 0,
pvzReturnsChange: 0,
}
);
}, [filteredAndSortedStores]);
const formatNumber = (num: number) => {
return num.toLocaleString("ru-RU");
};
const formatChange = (change: number) => {
const sign = change > 0 ? "+" : "";
return `${sign}${change}`;
};
const toggleStoreExpansion = (storeId: string) => {
const newExpanded = new Set(expandedStores);
if (newExpanded.has(storeId)) {
newExpanded.delete(storeId);
} else {
newExpanded.add(storeId);
}
setExpandedStores(newExpanded);
};
const handleSort = (field: keyof StoreData) => {
if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortField(field);
setSortOrder("asc");
}
};
// Компонент компактной статистической карточки
const StatCard = ({
title,
icon: Icon,
current,
change,
description,
}: {
title: string;
icon: React.ComponentType<{ className?: string }>;
current: number;
change: number;
description: string;
}) => (
<div
className={`glass-card p-3 hover:bg-white/15 transition-all duration-300 relative overflow-hidden`}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-white/10 rounded-lg">
<Icon className="h-3 w-3 text-white" />
</div>
<span className="text-white text-xs font-semibold">{title}</span>
</div>
<div
className={`flex items-center space-x-1 px-1.5 py-0.5 rounded-full ${
change >= 0 ? "bg-green-500/20" : "bg-red-500/20"
}`}
>
{change >= 0 ? (
<TrendingUp className="h-2.5 w-2.5 text-green-400" />
) : (
<TrendingDown className="h-2.5 w-2.5 text-red-400" />
)}
<span
className={`text-[10px] font-bold ${
change >= 0 ? "text-green-400" : "text-red-400"
}`}
>
{formatChange(change)}
</span>
</div>
</div>
<div className="text-lg font-bold text-white mb-1">
{formatNumber(current)}
</div>
<div className="text-white/60 text-[10px]">{description}</div>
</div>
);
// Компонент заголовка таблицы
const TableHeader = ({
field,
children,
sortable = false,
}: {
field?: keyof StoreData;
children: React.ReactNode;
sortable?: boolean;
}) => (
<div
className={`px-3 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider ${
sortable ? "cursor-pointer hover:text-white hover:bg-white/5" : ""
} flex items-center space-x-1`}
onClick={sortable && field ? () => handleSort(field) : undefined}
>
<span>{children}</span>
{sortable && field && (
<ArrowUpDown
className={`h-3 w-3 ${
sortField === field ? "text-blue-400" : "text-white/40"
}`}
/>
)}
</div>
);
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold text-white mb-4">
Склад фулфилмент - 2
</h2>
<p className="text-white/70 mb-6">
Обновленная версия компонента склада фулфилмента с оптимизацией для
компактных экранов
</p>
</div>
<Card
className="glass-card p-6 overflow-hidden"
style={{ height: "700px" }}
>
<div className="h-full flex flex-col">
{/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30%" }}>
<div className="glass-card p-4">
<h3 className="text-base font-semibold text-blue-400 mb-3">
Статистика склада
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
<StatCard
title="Продукты"
icon={Box}
current={warehouseStats.products.current}
change={warehouseStats.products.change}
description="Готовые к отправке"
/>
<StatCard
title="Товары"
icon={Package}
current={warehouseStats.goods.current}
change={warehouseStats.goods.change}
description="В обработке"
/>
<StatCard
title="Брак"
icon={AlertTriangle}
current={warehouseStats.defects.current}
change={warehouseStats.defects.change}
description="Требует утилизации"
/>
<StatCard
title="Возвраты с ПВЗ"
icon={RotateCcw}
current={warehouseStats.pvzReturns.current}
change={warehouseStats.pvzReturns.change}
description="К обработке"
/>
<StatCard
title="Расходники ФФ"
icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change}
description="Упаковка, этикетки"
/>
<StatCard
title="Расходники селлеров"
icon={Users}
current={warehouseStats.sellerSupplies.current}
change={warehouseStats.sellerSupplies.change}
description="Материалы клиентов"
/>
</div>
</div>
</div>
{/* Основная скроллируемая часть - оставшиеся 70% экрана */}
<div
className="flex-1 flex flex-col overflow-hidden"
style={{ minHeight: "60%" }}
>
<div className="glass-card flex-1 flex flex-col overflow-hidden">
{/* Компактная шапка таблицы - максимум 10% экрана */}
<div
className="p-4 border-b border-white/10 flex-shrink-0"
style={{ maxHeight: "10%" }}
>
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold text-white">
Детализация по магазинам
</h3>
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs"
>
{filteredAndSortedStores.length} магазинов
</Badge>
</div>
{/* Компактный поиск */}
<div className="relative">
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
<Input
placeholder="Поиск по магазинам..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40"
/>
</div>
</div>
{/* Фиксированные заголовки таблицы */}
<div className="flex-shrink-0 bg-white/5 border-b border-white/10">
<div className="grid grid-cols-7 gap-0">
<TableHeader field="name" sortable>
/ Магазин
</TableHeader>
<TableHeader field="products" sortable>
Продукты
</TableHeader>
<TableHeader field="goods" sortable>
Товары
</TableHeader>
<TableHeader field="defects" sortable>
Брак
</TableHeader>
<TableHeader field="sellerSupplies" sortable>
Расходники селлера
</TableHeader>
<TableHeader field="pvzReturns" sortable>
Возвраты с ПВЗ
</TableHeader>
<TableHeader>Действия</TableHeader>
</div>
</div>
{/* Строка с суммами */}
<div className="flex-shrink-0 bg-blue-500/10 border-b border-blue-500/20">
<div className="grid grid-cols-7 gap-0">
<div className="px-3 py-2 text-xs font-bold text-blue-300">
ИТОГО ({filteredAndSortedStores.length})
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatNumber(totals.products)}
<div
className={`text-[10px] ${
totals.productsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(totals.productsChange)}
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatNumber(totals.goods)}
<div
className={`text-[10px] ${
totals.goodsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(totals.goodsChange)}
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatNumber(totals.defects)}
<div
className={`text-[10px] ${
totals.defectsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(totals.defectsChange)}
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatNumber(totals.sellerSupplies)}
<div
className={`text-[10px] ${
totals.sellerSuppliesChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(totals.sellerSuppliesChange)}
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatNumber(totals.pvzReturns)}
<div
className={`text-[10px] ${
totals.pvzReturnsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(totals.pvzReturnsChange)}
</div>
</div>
<div className="px-3 py-2"></div>
</div>
</div>
{/* Скроллируемый контент таблицы - оставшееся пространство */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
{filteredAndSortedStores.map((store, index) => (
<div
key={store.id}
className="border-b border-white/10 hover:bg-white/5 transition-colors"
>
{/* Основная строка магазина */}
<div className="grid grid-cols-7 gap-0">
<div className="px-3 py-2.5 flex items-center space-x-2">
<span className="text-white/60 text-xs">
{index + 1}
</span>
<div className="flex items-center space-x-2">
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-500 rounded-md flex items-center justify-center">
<Store className="h-3 w-3 text-white" />
</div>
<div>
<div className="text-white font-medium text-xs">
{store.name}
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="text-white font-semibold text-sm">
{formatNumber(store.products)}
</div>
<div
className={`text-[10px] ${
store.productsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(store.productsChange)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="text-white font-semibold text-sm">
{formatNumber(store.goods)}
</div>
<div
className={`text-[10px] ${
store.goodsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(store.goodsChange)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="text-white font-semibold text-sm">
{formatNumber(store.defects)}
</div>
<div
className={`text-[10px] ${
store.defectsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(store.defectsChange)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="text-white font-semibold text-sm">
{formatNumber(store.sellerSupplies)}
</div>
<div
className={`text-[10px] ${
store.sellerSuppliesChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(store.sellerSuppliesChange)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="text-white font-semibold text-sm">
{formatNumber(store.pvzReturns)}
</div>
<div
className={`text-[10px] ${
store.pvzReturnsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{formatChange(store.pvzReturnsChange)}
</div>
</div>
<div className="px-3 py-2.5">
<Button
variant="ghost"
size="sm"
onClick={() => toggleStoreExpansion(store.id)}
className="text-white/60 hover:text-white hover:bg-white/10 h-6 w-6 p-0"
>
{expandedStores.has(store.id) ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
</Button>
</div>
</div>
{/* Расширенная информация */}
{expandedStores.has(store.id) && (
<div className="bg-white/5 px-3 py-3 border-t border-white/10">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<Package2 className="h-3 w-3 text-blue-400" />
<span className="text-blue-300 text-xs font-medium">
Продукты
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.products)}
</div>
<div className="text-blue-200/60 text-[10px]">
Готовые к отправке
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<Package className="h-3 w-3 text-cyan-400" />
<span className="text-cyan-300 text-xs font-medium">
Товары
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.goods)}
</div>
<div className="text-cyan-200/60 text-[10px]">
В обработке
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<AlertTriangle className="h-3 w-3 text-red-400" />
<span className="text-red-300 text-xs font-medium">
Брак
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.defects)}
</div>
<div className="text-red-200/60 text-[10px]">
К утилизации
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<RotateCcw className="h-3 w-3 text-yellow-400" />
<span className="text-yellow-300 text-xs font-medium">
Возвраты
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.pvzReturns)}
</div>
<div className="text-yellow-200/60 text-[10px]">
С ПВЗ
</div>
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
</Card>
</div>
);
}

View File

@ -1,437 +1,943 @@
"use client"
"use client";
import { useState, useEffect } from 'react'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Sidebar } from '@/components/dashboard/sidebar'
import { useSidebar } from '@/hooks/useSidebar'
import { StatsCard } from '@/components/supplies/ui/stats-card'
import { StatsGrid } from '@/components/supplies/ui/stats-grid'
import { useState, useMemo } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import {
Package,
TrendingUp,
TrendingDown,
AlertTriangle,
RotateCcw,
Wrench,
Users,
ShoppingBag,
ChevronDown,
ChevronUp,
Box,
Zap,
Target,
Activity,
BarChart3,
Eye,
EyeOff,
Warehouse
} from 'lucide-react'
Search,
ArrowUpDown,
Store,
Package2,
} from "lucide-react";
// Типы данных
interface StoreData {
id: string;
name: string;
logo?: string;
products: number;
goods: number;
defects: number;
sellerSupplies: number;
pvzReturns: number;
// Изменения за сутки
productsChange: number;
goodsChange: number;
defectsChange: number;
sellerSuppliesChange: number;
pvzReturnsChange: number;
}
interface WarehouseStats {
products: { current: number; change: number };
goods: { current: number; change: number };
defects: { current: number; change: number };
pvzReturns: { current: number; change: number };
fulfillmentSupplies: { current: number; change: number };
sellerSupplies: { current: number; change: number };
}
export function FulfillmentWarehouseDashboard() {
const { getSidebarMargin } = useSidebar()
const { getSidebarMargin } = useSidebar();
// Состояния для свёртывания блоков
const [expandedSections, setExpandedSections] = useState({
warehouse: true
})
// Состояния для поиска и фильтрации
const [searchTerm, setSearchTerm] = useState("");
const [sortField, setSortField] = useState<keyof StoreData>("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set());
// Состояние для живых изменений продуктов
const [liveChange, setLiveChange] = useState({
value: 12,
isPositive: true,
timestamp: Date.now()
})
// Мок данные для статистики
const warehouseStats: WarehouseStats = {
products: { current: 2856, change: 124 },
goods: { current: 1391, change: 87 },
defects: { current: 43, change: -12 },
pvzReturns: { current: 256, change: 34 },
fulfillmentSupplies: { current: 189, change: 23 },
sellerSupplies: { current: 534, change: 67 },
};
// Состояние для модуля товары с дополнительными значениями
const [goodsData, setGoodsData] = useState({
processing: 245, // В обработке (положительное)
rejected: 18, // Отклонено (отрицательное)
efficiency: 87, // Эффективность обработки
isActive: true, // Активность процесса
pulse: 0 // Для анимации пульса
})
// Мок данные для магазинов
const mockStoreData: StoreData[] = useMemo(
() => [
{
id: "1",
name: "Электроника Плюс",
products: 456,
goods: 234,
defects: 12,
sellerSupplies: 89,
pvzReturns: 45,
productsChange: 23,
goodsChange: 15,
defectsChange: -3,
sellerSuppliesChange: 12,
pvzReturnsChange: 8,
},
{
id: "2",
name: "Мода и Стиль",
products: 678,
goods: 345,
defects: 8,
sellerSupplies: 123,
pvzReturns: 67,
productsChange: 34,
goodsChange: 22,
defectsChange: -2,
sellerSuppliesChange: 18,
pvzReturnsChange: 12,
},
{
id: "3",
name: "Дом и Сад",
products: 289,
goods: 156,
defects: 5,
sellerSupplies: 67,
pvzReturns: 23,
productsChange: 12,
goodsChange: 8,
defectsChange: -1,
sellerSuppliesChange: 9,
pvzReturnsChange: 4,
},
{
id: "4",
name: "Спорт и Отдых",
products: 567,
goods: 289,
defects: 15,
sellerSupplies: 134,
pvzReturns: 78,
productsChange: 28,
goodsChange: 19,
defectsChange: -4,
sellerSuppliesChange: 21,
pvzReturnsChange: 15,
},
{
id: "5",
name: "Красота и Здоровье",
products: 234,
goods: 123,
defects: 3,
sellerSupplies: 45,
pvzReturns: 19,
productsChange: 8,
goodsChange: 5,
defectsChange: 0,
sellerSuppliesChange: 6,
pvzReturnsChange: 3,
},
],
[]
);
// Симуляция живых изменений для продуктов
useEffect(() => {
const interval = setInterval(() => {
const change = Math.floor(Math.random() * 20) - 10 // от -10 до +10
setLiveChange({
value: Math.abs(change),
isPositive: change >= 0,
timestamp: Date.now()
})
}, 3000) // каждые 3 секунды
// Фильтрация и сортировка данных
const filteredAndSortedStores = useMemo(() => {
const filtered = mockStoreData.filter((store) =>
store.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return () => clearInterval(interval)
}, [])
filtered.sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
// Симуляция изменений для товаров
useEffect(() => {
const interval = setInterval(() => {
setGoodsData(prev => ({
...prev,
processing: prev.processing + Math.floor(Math.random() * 10) - 5,
rejected: Math.max(0, prev.rejected + Math.floor(Math.random() * 6) - 3),
efficiency: Math.min(100, Math.max(70, prev.efficiency + Math.floor(Math.random() * 6) - 3)),
pulse: prev.pulse + 1
}))
}, 2500) // каждые 2.5 секунды
if (typeof aValue === "string" && typeof bValue === "string") {
return sortOrder === "asc"
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}
return () => clearInterval(interval)
}, [])
if (typeof aValue === "number" && typeof bValue === "number") {
return sortOrder === "asc" ? aValue - bValue : bValue - aValue;
}
// Мок данные для статистики склада фулфилмента
const warehouseStats = {
// Текущие данные
currentProducts: 856, // Готовые продукты
currentGoods: 391, // Товары в процессе
currentDefects: 23,
currentReturns: 156,
currentFulfillmentSupplies: 89,
currentSellerSupplies: 234,
return 0;
});
return filtered;
}, [searchTerm, sortField, sortOrder, mockStoreData]);
// Тренды (в процентах)
productsTrend: 12,
goodsTrend: 8,
defectsTrend: -5,
returnsTrend: 8,
suppliesTrend: 15,
// Дополнительная аналитика
efficiency: 94.5,
turnover: 2.3,
utilizationRate: 87
}
// Подсчет общих сумм
const totals = useMemo(() => {
return filteredAndSortedStores.reduce(
(acc, store) => ({
products: acc.products + store.products,
goods: acc.goods + store.goods,
defects: acc.defects + store.defects,
sellerSupplies: acc.sellerSupplies + store.sellerSupplies,
pvzReturns: acc.pvzReturns + store.pvzReturns,
productsChange: acc.productsChange + store.productsChange,
goodsChange: acc.goodsChange + store.goodsChange,
defectsChange: acc.defectsChange + store.defectsChange,
sellerSuppliesChange:
acc.sellerSuppliesChange + store.sellerSuppliesChange,
pvzReturnsChange: acc.pvzReturnsChange + store.pvzReturnsChange,
}),
{
products: 0,
goods: 0,
defects: 0,
sellerSupplies: 0,
pvzReturns: 0,
productsChange: 0,
goodsChange: 0,
defectsChange: 0,
sellerSuppliesChange: 0,
pvzReturnsChange: 0,
}
);
}, [filteredAndSortedStores]);
const formatNumber = (num: number) => {
return num.toLocaleString('ru-RU')
}
return num.toLocaleString("ru-RU");
};
const toggleSection = (section: keyof typeof expandedSections) => {
setExpandedSections(prev => ({
...prev,
[section]: !prev[section]
}))
}
const formatChange = (change: number) => {
const sign = change > 0 ? "+" : "";
return `${sign}${change}`;
};
// Компонент заголовка секции с кнопкой свёртывания
const SectionHeader = ({ title, section, badge, color = "text-white" }: {
title: string
section: keyof typeof expandedSections
badge?: number
color?: string
}) => (
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<h2 className={`text-lg font-semibold ${color}`}>{title}</h2>
{badge && (
<span className="px-2 py-1 bg-blue-500/20 text-blue-300 text-xs rounded-full font-medium">
{badge}
</span>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => toggleSection(section)}
className="text-white/60 hover:text-white hover:bg-white/10 p-1 h-8 w-8"
const toggleStoreExpansion = (storeId: string) => {
const newExpanded = new Set(expandedStores);
if (newExpanded.has(storeId)) {
newExpanded.delete(storeId);
} else {
newExpanded.add(storeId);
}
setExpandedStores(newExpanded);
};
const handleSort = (field: keyof StoreData) => {
if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortField(field);
setSortOrder("asc");
}
};
// Компонент компактной статистической карточки
const StatCard = ({
title,
icon: Icon,
current,
change,
description,
}: {
title: string;
icon: React.ComponentType<{ className?: string }>;
current: number;
change: number;
description: string;
}) => {
// Генерируем случайные значения для положительных и отрицательных изменений
const positiveChange = Math.floor(Math.random() * 50) + 10; // от 10 до 59
const negativeChange = Math.floor(Math.random() * 30) + 5; // от 5 до 34
const percentChange = (change / current) * 100;
return (
<div
className={`glass-card p-3 hover:bg-white/15 transition-all duration-300 relative overflow-hidden`}
>
{expandedSections[section] ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-white/10 rounded-lg">
<Icon className="h-3 w-3 text-white" />
</div>
<span className="text-white text-xs font-semibold">{title}</span>
</div>
{/* Процентное изменение */}
<div className="flex items-center space-x-0.5 px-1.5 py-0.5 rounded bg-blue-500/20">
{change >= 0 ? (
<TrendingUp className="h-3 w-3 text-green-400" />
) : (
<TrendingDown className="h-3 w-3 text-red-400" />
)}
<span
className={`text-xs font-bold ${
change >= 0 ? "text-green-400" : "text-red-400"
}`}
>
{percentChange.toFixed(1)}%
</span>
</div>
</div>
<div className="flex items-center justify-between mb-1">
<div className="text-lg font-bold text-white">
{formatNumber(current)}
</div>
<div className="flex items-center space-x-1">
{/* Положительное изменение */}
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-green-500/20">
<span className="text-xs font-bold text-green-400">
+{positiveChange}
</span>
</div>
{/* Отрицательное изменение */}
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-red-500/20">
<span className="text-xs font-bold text-red-400">
-{negativeChange}
</span>
</div>
</div>
</div>
<div className="text-white/60 text-[10px]">{description}</div>
</div>
);
};
// Компонент заголовка таблицы
const TableHeader = ({
field,
children,
sortable = false,
}: {
field?: keyof StoreData;
children: React.ReactNode;
sortable?: boolean;
}) => (
<div
className={`px-3 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider ${
sortable ? "cursor-pointer hover:text-white hover:bg-white/5" : ""
} flex items-center space-x-1`}
onClick={sortable && field ? () => handleSort(field) : undefined}
>
<span>{children}</span>
{sortable && field && (
<ArrowUpDown
className={`h-3 w-3 ${
sortField === field ? "text-blue-400" : "text-white/40"
}`}
/>
)}
</div>
)
);
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-y-auto transition-all duration-300`}>
<div className="w-full">
{/* Блок состояния склада с shadcn/ui */}
<Card className="mb-6 bg-gradient-to-br from-slate-900/50 to-slate-800/30 border-slate-700/50">
<div className="p-6">
<SectionHeader
title="Состояние склада"
section="warehouse"
badge={warehouseStats.currentProducts + warehouseStats.currentGoods + warehouseStats.currentFulfillmentSupplies + warehouseStats.currentSellerSupplies}
color="text-blue-400"
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300`}
>
{/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
<div className="glass-card p-4">
<h2 className="text-base font-semibold text-blue-400 mb-3">
Статистика склада
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
<StatCard
title="Продукты"
icon={Box}
current={warehouseStats.products.current}
change={warehouseStats.products.change}
description="Готовые к отправке"
/>
<StatCard
title="Товары"
icon={Package}
current={warehouseStats.goods.current}
change={warehouseStats.goods.change}
description="В обработке"
/>
<StatCard
title="Брак"
icon={AlertTriangle}
current={warehouseStats.defects.current}
change={warehouseStats.defects.change}
description="Требует утилизации"
/>
<StatCard
title="Возвраты с ПВЗ"
icon={RotateCcw}
current={warehouseStats.pvzReturns.current}
change={warehouseStats.pvzReturns.change}
description="К обработке"
/>
<StatCard
title="Расходники ФФ"
icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change}
description="Упаковка, этикетки"
/>
<StatCard
title="Расходники селлеров"
icon={Users}
current={warehouseStats.sellerSupplies.current}
change={warehouseStats.sellerSupplies.change}
description="Материалы клиентов"
/>
{expandedSections.warehouse && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4 mt-4">
{/* Уникальный модуль "Продукты" */}
<div className="min-w-0 relative group">
<div className="bg-gradient-to-br from-blue-500/10 via-blue-400/5 to-cyan-500/10 backdrop-blur border border-blue-400/30 rounded-2xl p-4 hover:from-blue-500/20 hover:to-cyan-500/20 transition-all duration-500 hover:scale-[1.02] hover:shadow-2xl hover:shadow-blue-500/20">
{/* Живой индикатор изменений */}
<div className="absolute -top-1 -right-1 flex items-center space-x-1">
<div className="relative">
<div className={`w-3 h-3 rounded-full animate-pulse shadow-lg ${
liveChange.isPositive
? 'bg-green-500 shadow-green-500/50'
: 'bg-red-500 shadow-red-500/50'
}`}></div>
<div className={`absolute inset-0 w-3 h-3 rounded-full animate-ping opacity-75 ${
liveChange.isPositive ? 'bg-green-500' : 'bg-red-500'
}`}></div>
</div>
<div className={`backdrop-blur text-white text-[10px] font-bold px-2 py-0.5 rounded-full animate-bounce ${
liveChange.isPositive
? 'bg-green-500/90'
: 'bg-red-500/90'
}`}>
{liveChange.isPositive ? '+' : '-'}{liveChange.value}
</div>
</div>
{/* Заголовок с иконкой */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="relative">
<div className="p-2 bg-blue-500/20 rounded-xl border border-blue-400/30">
<Box className="h-4 w-4 text-blue-400" />
</div>
<div className={`absolute -top-1 -right-1 w-2 h-2 rounded-full animate-pulse ${
liveChange.isPositive ? 'bg-green-400' : 'bg-red-400'
}`}></div>
</div>
<span className="text-blue-100 text-sm font-semibold tracking-wide">ПРОДУКТЫ</span>
</div>
{/* Мини-график тренда */}
<div className="flex items-center space-x-1">
<div className="flex items-end space-x-0.5 h-4">
<div className={`w-1 rounded-full ${liveChange.isPositive ? 'bg-green-400/60' : 'bg-red-400/60'}`} style={{height: '60%'}}></div>
<div className={`w-1 rounded-full ${liveChange.isPositive ? 'bg-green-400/70' : 'bg-red-400/70'}`} style={{height: '80%'}}></div>
<div className={`w-1 rounded-full ${liveChange.isPositive ? 'bg-green-400/80' : 'bg-red-400/80'}`} style={{height: '70%'}}></div>
<div className={`w-1 rounded-full animate-pulse ${liveChange.isPositive ? 'bg-green-400' : 'bg-red-400'}`} style={{height: '100%'}}></div>
</div>
</div>
</div>
{/* Основное значение */}
<div className="mb-2">
<div className="flex items-baseline space-x-2">
<span className="text-2xl font-black text-white tracking-tight">
{formatNumber(warehouseStats.currentProducts)}
</span>
<div className={`flex items-center space-x-1 px-2 py-1 rounded-full ${
liveChange.isPositive ? 'bg-green-500/20' : 'bg-red-500/20'
}`}>
<svg className={`w-3 h-3 ${liveChange.isPositive ? 'text-green-400' : 'text-red-400'}`} fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d={liveChange.isPositive
? "M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z"
: "M14.707 12.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l2.293-2.293a1 1 0 011.414 0z"
} clipRule="evenodd" />
</svg>
<span className={`text-xs font-bold ${liveChange.isPositive ? 'text-green-400' : 'text-red-400'}`}>
{liveChange.value}%
</span>
</div>
</div>
</div>
{/* Подпись */}
<div className="text-blue-200/70 text-xs">
Готовые к отправке
</div>
{/* Прогресс-бар */}
<div className="mt-3 relative">
<div className="h-1.5 bg-blue-900/30 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-400 to-cyan-400 rounded-full transition-all duration-1000 ease-out relative"
style={{width: '78%'}}
>
<div className="absolute right-0 top-0 h-full w-4 bg-gradient-to-r from-transparent to-white/30 animate-pulse"></div>
</div>
</div>
<div className="flex justify-between text-[10px] text-blue-300/60 mt-1">
<span>0</span>
<span>1.2К</span>
</div>
</div>
{/* Живое изменение значения */}
<div className="absolute bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className={`flex items-center space-x-1 backdrop-blur px-2 py-1 rounded-lg ${
liveChange.isPositive ? 'bg-green-500/90' : 'bg-red-500/90'
}`}>
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
<span className="text-white text-[10px] font-bold">
LIVE {liveChange.isPositive ? '+' : '-'}{liveChange.value}
</span>
</div>
</div>
{/* Декоративные элементы */}
<div className="absolute top-1 left-1 w-8 h-8 bg-gradient-to-br from-blue-400/10 to-transparent rounded-full"></div>
<div className="absolute bottom-1 right-1 w-6 h-6 bg-gradient-to-tl from-cyan-400/10 to-transparent rounded-full"></div>
</div>
</div>
{/* Компактный модуль "Товары" - той же высоты что и "Продукты" */}
<div className="min-w-0 relative group">
<div className="bg-gradient-to-br from-cyan-500/10 via-teal-400/5 to-emerald-500/10 backdrop-blur border border-cyan-400/30 rounded-2xl p-4 hover:from-cyan-500/20 hover:to-emerald-500/20 transition-all duration-500 hover:shadow-2xl hover:shadow-cyan-500/20 relative overflow-hidden">
{/* Заголовок с иконкой */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="relative">
<div className="p-2 bg-cyan-500/20 rounded-xl border border-cyan-400/30 relative">
<Package className="h-4 w-4 text-cyan-400" />
</div>
</div>
<span className="text-cyan-100 text-sm font-semibold tracking-wide">ТОВАРЫ</span>
</div>
{/* Индикатор эффективности */}
<div className="relative w-8 h-8">
<svg className="w-8 h-8 transform -rotate-90" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14" stroke="currentColor" strokeWidth="2" fill="none" className="text-cyan-900/30" />
<circle
cx="16" cy="16" r="14"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeDasharray={`${goodsData.efficiency * 0.88} 88`}
className="text-cyan-400 transition-all duration-1000"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-[8px] font-bold text-cyan-300">{goodsData.efficiency}%</span>
</div>
</div>
</div>
{/* Основное значение */}
<div className="mb-3">
<div className="flex items-baseline space-x-2">
<span className="text-2xl font-black text-white tracking-tight">
{formatNumber(warehouseStats.currentGoods)}
</span>
</div>
</div>
{/* В обработке с числовым значением */}
<div className="text-cyan-200/70 text-xs mb-2">
В обработке: <span className="text-white font-semibold">{formatNumber(goodsData.processing + goodsData.rejected)}</span>
</div>
{/* Прогресс-бар */}
<div className="relative h-1.5 bg-cyan-900/30 rounded-full overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-emerald-400 rounded-full" style={{
width: `${(goodsData.processing / (goodsData.processing + goodsData.rejected)) * 100}%`
}}></div>
</div>
<div className="flex justify-between text-[10px] text-cyan-300/60 mt-1">
<span>Поставлено: {((goodsData.processing / (goodsData.processing + goodsData.rejected)) * 100).toFixed(0)}%</span>
<span>Отправлено: {((goodsData.rejected / (goodsData.processing + goodsData.rejected)) * 100).toFixed(0)}%</span>
</div>
{/* Декоративные элементы */}
<div className="absolute top-1 left-1 w-8 h-8 bg-gradient-to-br from-cyan-400/10 to-transparent rounded-full"></div>
<div className="absolute bottom-1 right-1 w-6 h-6 bg-gradient-to-tl from-emerald-400/10 to-transparent rounded-full"></div>
</div>
</div>
{/* Брак */}
<Card className="bg-gradient-to-br from-red-500/10 to-red-600/5 border-red-500/20 hover:border-red-400/30 transition-all duration-300">
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="p-2 bg-red-500/20 rounded-xl">
<AlertTriangle className="h-4 w-4 text-red-400" />
</div>
<span className="text-red-100 text-sm font-semibold">Брак</span>
</div>
<Badge variant="secondary" className="bg-red-500/20 text-red-300 text-xs">
-{Math.abs(warehouseStats.defectsTrend)}%
</Badge>
</div>
<div className="text-2xl font-bold text-white mb-1">
{formatNumber(warehouseStats.currentDefects)}
</div>
<div className="text-red-200/60 text-xs">
Требует утилизации
</div>
</div>
</Card>
{/* Возвраты с ПВЗ */}
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-600/5 border-yellow-500/20 hover:border-yellow-400/30 transition-all duration-300">
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="p-2 bg-yellow-500/20 rounded-xl">
<RotateCcw className="h-4 w-4 text-yellow-400" />
</div>
<span className="text-yellow-100 text-sm font-semibold">Возвраты с ПВЗ</span>
</div>
<Badge variant="secondary" className="bg-yellow-500/20 text-yellow-300 text-xs">
+{warehouseStats.returnsTrend}%
</Badge>
</div>
<div className="text-2xl font-bold text-white mb-1">
{formatNumber(warehouseStats.currentReturns)}
</div>
<div className="text-yellow-200/60 text-xs">
К обработке
</div>
</div>
</Card>
{/* Расходники ФФ */}
<Card className="bg-gradient-to-br from-purple-500/10 to-purple-600/5 border-purple-500/20 hover:border-purple-400/30 transition-all duration-300">
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="p-2 bg-purple-500/20 rounded-xl">
<Wrench className="h-4 w-4 text-purple-400" />
</div>
<span className="text-purple-100 text-sm font-semibold">Расходники ФФ</span>
</div>
</div>
<div className="text-2xl font-bold text-white mb-1">
{formatNumber(warehouseStats.currentFulfillmentSupplies)}
</div>
<div className="text-purple-200/60 text-xs">
Упаковка, этикетки, пленка
</div>
</div>
</Card>
{/* Расходники селлеров */}
<Card className="bg-gradient-to-br from-green-500/10 to-green-600/5 border-green-500/20 hover:border-green-400/30 transition-all duration-300">
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="p-2 bg-green-500/20 rounded-xl">
<Users className="h-4 w-4 text-green-400" />
</div>
<span className="text-green-100 text-sm font-semibold">Расходники селлеров</span>
</div>
</div>
<div className="text-2xl font-bold text-white mb-1">
{formatNumber(warehouseStats.currentSellerSupplies)}
</div>
<div className="text-green-200/60 text-xs">
Материалы клиентов
</div>
</div>
</Card>
</div>
)}
</div>
</Card>
</div>
</div>
{/* Основная скроллируемая часть - оставшиеся 70% экрана */}
<div
className="flex-1 flex flex-col overflow-hidden"
style={{ minHeight: "60vh" }}
>
<div className="glass-card flex-1 flex flex-col overflow-hidden">
{/* Компактная шапка таблицы - максимум 10% экрана */}
<div
className="p-4 border-b border-white/10 flex-shrink-0"
style={{ maxHeight: "10vh" }}
>
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold text-white">
Детализация по магазинам
</h2>
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs"
>
{filteredAndSortedStores.length} магазинов
</Badge>
</div>
{/* Компактный поиск */}
<div className="relative">
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
<Input
placeholder="Поиск по магазинам..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40"
/>
</div>
</div>
{/* Фиксированные заголовки таблицы */}
<div className="flex-shrink-0 bg-white/5 border-b border-white/10">
<div className="grid grid-cols-6 gap-0">
<TableHeader field="name" sortable>
/ Магазин
</TableHeader>
<TableHeader field="products" sortable>
Продукты
</TableHeader>
<TableHeader field="goods" sortable>
Товары
</TableHeader>
<TableHeader field="defects" sortable>
Брак
</TableHeader>
<TableHeader field="sellerSupplies" sortable>
Расходники селлера
</TableHeader>
<TableHeader field="pvzReturns" sortable>
Возвраты с ПВЗ
</TableHeader>
</div>
</div>
{/* Строка с суммами */}
<div className="flex-shrink-0 bg-blue-500/10 border-b border-blue-500/20">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-2 text-xs font-bold text-blue-300">
ИТОГО ({filteredAndSortedStores.length})
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.products)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.productsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.productsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{(
(totals.productsChange / totals.products) *
100
).toFixed(1)}
%
</span>
</div>
</div>
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.productsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.productsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.productsChange)}
</span>
</div>
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.goods)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.goodsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.goodsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{((totals.goodsChange / totals.goods) * 100).toFixed(1)}
%
</span>
</div>
</div>
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.goodsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.goodsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.goodsChange)}
</span>
</div>
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.defects)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.defectsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.defectsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{(
(totals.defectsChange / totals.defects) *
100
).toFixed(1)}
%
</span>
</div>
</div>
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.defectsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.defectsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.defectsChange)}
</span>
</div>
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.sellerSupplies)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.sellerSuppliesChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.sellerSuppliesChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{(
(totals.sellerSuppliesChange /
totals.sellerSupplies) *
100
).toFixed(1)}
%
</span>
</div>
</div>
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
{Math.abs(
Math.floor(totals.sellerSuppliesChange * 0.6)
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
{Math.abs(
Math.floor(totals.sellerSuppliesChange * 0.4)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.sellerSuppliesChange)}
</span>
</div>
</div>
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.pvzReturns)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.pvzReturnsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.pvzReturnsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{(
(totals.pvzReturnsChange / totals.pvzReturns) *
100
).toFixed(1)}
%
</span>
</div>
</div>
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.pvzReturnsChange)}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Скроллируемый контент таблицы - оставшееся пространство */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
{filteredAndSortedStores.map((store, index) => (
<div
key={store.id}
className="border-b border-white/10 hover:bg-white/5 transition-colors"
>
{/* Основная строка магазина */}
<div
className="grid grid-cols-6 gap-0 cursor-pointer"
onClick={() => toggleStoreExpansion(store.id)}
>
<div className="px-3 py-2.5 flex items-center space-x-2">
<span className="text-white/60 text-xs">
{filteredAndSortedStores.length - index}
</span>
<div className="flex items-center space-x-2">
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-500 rounded-md flex items-center justify-center">
<Store className="h-3 w-3 text-white" />
</div>
<div>
<div className="text-white font-medium text-xs">
{store.name}
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
{formatNumber(store.products)}
</div>
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
{Math.abs(Math.floor(store.productsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
{Math.abs(Math.floor(store.productsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.productsChange)}
</span>
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
{formatNumber(store.goods)}
</div>
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(store.goodsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(store.goodsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.goodsChange)}
</span>
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
{formatNumber(store.defects)}
</div>
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(store.defectsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(store.defectsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.defectsChange)}
</span>
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
{formatNumber(store.sellerSupplies)}
</div>
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
{Math.abs(
Math.floor(store.sellerSuppliesChange * 0.6)
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
{Math.abs(
Math.floor(store.sellerSuppliesChange * 0.4)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.sellerSuppliesChange)}
</span>
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
{formatNumber(store.pvzReturns)}
</div>
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
{Math.abs(
Math.floor(store.pvzReturnsChange * 0.6)
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
{Math.abs(
Math.floor(store.pvzReturnsChange * 0.4)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.pvzReturnsChange)}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Расширенная информация */}
{expandedStores.has(store.id) && (
<div className="bg-white/5 px-3 py-3 border-t border-white/10">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<Package2 className="h-3 w-3 text-blue-400" />
<span className="text-blue-300 text-xs font-medium">
Продукты
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.products)}
</div>
<div className="text-blue-200/60 text-[10px]">
Готовые к отправке
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<Package className="h-3 w-3 text-cyan-400" />
<span className="text-cyan-300 text-xs font-medium">
Товары
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.goods)}
</div>
<div className="text-cyan-200/60 text-[10px]">
В обработке
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<AlertTriangle className="h-3 w-3 text-red-400" />
<span className="text-red-300 text-xs font-medium">
Брак
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.defects)}
</div>
<div className="text-red-200/60 text-[10px]">
К утилизации
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<RotateCcw className="h-3 w-3 text-yellow-400" />
<span className="text-yellow-300 text-xs font-medium">
Возвраты
</span>
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.pvzReturns)}
</div>
<div className="text-yellow-200/60 text-[10px]">
С ПВЗ
</div>
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
</main>
</div>
)
);
}