From 25fead48e96cdda76c6ce1eb59190deffdc36f81 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Sat, 26 Jul 2025 17:21:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=B4=D0=B5=D0=BC?= =?UTF-8?q?=D0=BE-=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20FulfillmentWarehouse2Demo=20=D0=B2=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D1=83=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BC=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20UIKitSection=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=B4=D0=B5=D0=BC=D0=BE=20=D0=B8=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D1=85=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=BA.=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D1=81=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B8?= =?UTF-8?q?=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BE=20=D1=81=D0=BA=D0=BB=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/ui-kit-section.tsx | 13 +- .../ui-kit/fulfillment-warehouse-2-demo.tsx | 699 +++++++++ .../fulfillment-warehouse-dashboard.tsx | 1308 ++++++++++++----- 3 files changed, 1618 insertions(+), 402 deletions(-) create mode 100644 src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx diff --git a/src/components/admin/ui-kit-section.tsx b/src/components/admin/ui-kit-section.tsx index 6d3cea4..48625dd 100644 --- a/src/components/admin/ui-kit-section.tsx +++ b/src/components/admin/ui-kit-section.tsx @@ -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" > - Интерактив + Интерактивные Склад фулфилмент + + Склад фулфилмент - 2 + + + + + diff --git a/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx b/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx new file mode 100644 index 0000000..8b36190 --- /dev/null +++ b/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx @@ -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("name"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [expandedStores, setExpandedStores] = useState>(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; + }) => ( +
+
+
+
+ +
+ {title} +
+
= 0 ? "bg-green-500/20" : "bg-red-500/20" + }`} + > + {change >= 0 ? ( + + ) : ( + + )} + = 0 ? "text-green-400" : "text-red-400" + }`} + > + {formatChange(change)} + +
+
+
+ {formatNumber(current)} +
+
{description}
+
+ ); + + // Компонент заголовка таблицы + const TableHeader = ({ + field, + children, + sortable = false, + }: { + field?: keyof StoreData; + children: React.ReactNode; + sortable?: boolean; + }) => ( +
handleSort(field) : undefined} + > + {children} + {sortable && field && ( + + )} +
+ ); + + return ( +
+
+

+ Склад фулфилмент - 2 +

+

+ Обновленная версия компонента склада фулфилмента с оптимизацией для + компактных экранов +

+
+ + +
+ {/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */} +
+
+

+ Статистика склада +

+
+ + + + + + +
+
+
+ + {/* Основная скроллируемая часть - оставшиеся 70% экрана */} +
+
+ {/* Компактная шапка таблицы - максимум 10% экрана */} +
+
+

+ Детализация по магазинам +

+ + {filteredAndSortedStores.length} магазинов + +
+ + {/* Компактный поиск */} +
+ + setSearchTerm(e.target.value)} + className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40" + /> +
+
+ + {/* Фиксированные заголовки таблицы */} +
+
+ + № / Магазин + + + Продукты + + + Товары + + + Брак + + + Расходники селлера + + + Возвраты с ПВЗ + + Действия +
+
+ + {/* Строка с суммами */} +
+
+
+ ИТОГО ({filteredAndSortedStores.length}) +
+
+ {formatNumber(totals.products)} +
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(totals.productsChange)} +
+
+
+ {formatNumber(totals.goods)} +
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(totals.goodsChange)} +
+
+
+ {formatNumber(totals.defects)} +
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(totals.defectsChange)} +
+
+
+ {formatNumber(totals.sellerSupplies)} +
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(totals.sellerSuppliesChange)} +
+
+
+ {formatNumber(totals.pvzReturns)} +
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(totals.pvzReturnsChange)} +
+
+
+
+
+ + {/* Скроллируемый контент таблицы - оставшееся пространство */} +
+ {filteredAndSortedStores.map((store, index) => ( +
+ {/* Основная строка магазина */} +
+
+ + {index + 1} + +
+
+ +
+
+
+ {store.name} +
+
+
+
+ +
+
+ {formatNumber(store.products)} +
+
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(store.productsChange)} +
+
+ +
+
+ {formatNumber(store.goods)} +
+
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(store.goodsChange)} +
+
+ +
+
+ {formatNumber(store.defects)} +
+
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(store.defectsChange)} +
+
+ +
+
+ {formatNumber(store.sellerSupplies)} +
+
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(store.sellerSuppliesChange)} +
+
+ +
+
+ {formatNumber(store.pvzReturns)} +
+
= 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {formatChange(store.pvzReturnsChange)} +
+
+ +
+ +
+
+ + {/* Расширенная информация */} + {expandedStores.has(store.id) && ( +
+
+
+
+ + + Продукты + +
+
+ {formatNumber(store.products)} +
+
+ Готовые к отправке +
+
+ +
+
+ + + Товары + +
+
+ {formatNumber(store.goods)} +
+
+ В обработке +
+
+ +
+
+ + + Брак + +
+
+ {formatNumber(store.defects)} +
+
+ К утилизации +
+
+ +
+
+ + + Возвраты + +
+
+ {formatNumber(store.pvzReturns)} +
+
+ С ПВЗ +
+
+
+
+ )} +
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx index cd142ec..1b80ac7 100644 --- a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx +++ b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx @@ -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 [expandedSections, setExpandedSections] = useState({ - warehouse: true - }) + const { getSidebarMargin } = useSidebar(); - // Состояние для живых изменений продуктов - const [liveChange, setLiveChange] = useState({ - value: 12, - isPositive: true, - timestamp: Date.now() - }) + // Состояния для поиска и фильтрации + const [searchTerm, setSearchTerm] = useState(""); + const [sortField, setSortField] = useState("name"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [expandedStores, setExpandedStores] = useState>(new Set()); - // Состояние для модуля товары с дополнительными значениями - const [goodsData, setGoodsData] = useState({ - processing: 245, // В обработке (положительное) - rejected: 18, // Отклонено (отрицательное) - efficiency: 87, // Эффективность обработки - isActive: true, // Активность процесса - pulse: 0 // Для анимации пульса - }) + // Мок данные для статистики + 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 }, + }; - // Симуляция живых изменений для продуктов - 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 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, + }, + ], + [] + ); - return () => clearInterval(interval) - }, []) + // Фильтрация и сортировка данных + const filteredAndSortedStores = useMemo(() => { + const filtered = mockStoreData.filter((store) => + store.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); - // Симуляция изменений для товаров - 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 секунды + filtered.sort((a, b) => { + const aValue = a[sortField]; + const bValue = b[sortField]; - return () => clearInterval(interval) - }, []) + if (typeof aValue === "string" && typeof bValue === "string") { + return sortOrder === "asc" + ? aValue.localeCompare(bValue) + : bValue.localeCompare(aValue); + } - // Мок данные для статистики склада фулфилмента - const warehouseStats = { - // Текущие данные - currentProducts: 856, // Готовые продукты - currentGoods: 391, // Товары в процессе - currentDefects: 23, - currentReturns: 156, - currentFulfillmentSupplies: 89, - currentSellerSupplies: 234, - + if (typeof aValue === "number" && typeof bValue === "number") { + return sortOrder === "asc" ? aValue - bValue : bValue - aValue; + } - - // Тренды (в процентах) - productsTrend: 12, - goodsTrend: 8, - defectsTrend: -5, - returnsTrend: 8, - suppliesTrend: 15, - - // Дополнительная аналитика - efficiency: 94.5, - turnover: 2.3, - utilizationRate: 87 - } + 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') - } + 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 - }) => ( -
-
-

{title}

- {badge && ( - - {badge} - - )} -
- +
+
+
+ +
+ {title} +
+ {/* Процентное изменение */} +
+ {change >= 0 ? ( + + ) : ( + + )} + = 0 ? "text-green-400" : "text-red-400" + }`} + > + {percentChange.toFixed(1)}% + +
+
+
+
+ {formatNumber(current)} +
+
+ {/* Положительное изменение */} +
+ + +{positiveChange} + +
+ {/* Отрицательное изменение */} +
+ + -{negativeChange} + +
+
+
+
{description}
+
+ ); + }; + + // Компонент заголовка таблицы + const TableHeader = ({ + field, + children, + sortable = false, + }: { + field?: keyof StoreData; + children: React.ReactNode; + sortable?: boolean; + }) => ( +
handleSort(field) : undefined} + > + {children} + {sortable && field && ( + + )}
- ) + ); return (
-
-
- {/* Блок состояния склада с shadcn/ui */} - -
- + {/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */} +
+
+

+ Статистика склада +

+
+ + + + + + - {expandedSections.warehouse && ( -
- {/* Уникальный модуль "Продукты" */} -
-
- {/* Живой индикатор изменений */} -
-
-
-
-
-
- {liveChange.isPositive ? '+' : '-'}{liveChange.value} -
-
- - {/* Заголовок с иконкой */} -
-
-
-
- -
-
-
- ПРОДУКТЫ -
- - {/* Мини-график тренда */} -
-
-
-
-
-
-
-
-
- - {/* Основное значение */} -
-
- - {formatNumber(warehouseStats.currentProducts)} - -
- - - - - {liveChange.value}% - -
-
-
- - {/* Подпись */} -
- Готовые к отправке -
- - {/* Прогресс-бар */} -
-
-
-
-
-
-
- 0 - 1.2К -
-
- - {/* Живое изменение значения */} -
-
-
- - LIVE {liveChange.isPositive ? '+' : '-'}{liveChange.value} - -
-
- - {/* Декоративные элементы */} -
-
-
-
- - {/* Компактный модуль "Товары" - той же высоты что и "Продукты" */} -
-
- - {/* Заголовок с иконкой */} -
-
-
-
- -
-
- ТОВАРЫ -
- - {/* Индикатор эффективности */} -
- - - - -
- {goodsData.efficiency}% -
-
-
- - {/* Основное значение */} -
-
- - {formatNumber(warehouseStats.currentGoods)} - -
-
- - {/* В обработке с числовым значением */} -
- В обработке: {formatNumber(goodsData.processing + goodsData.rejected)} -
- - {/* Прогресс-бар */} -
-
-
-
- Поставлено: {((goodsData.processing / (goodsData.processing + goodsData.rejected)) * 100).toFixed(0)}% - Отправлено: {((goodsData.rejected / (goodsData.processing + goodsData.rejected)) * 100).toFixed(0)}% -
- - {/* Декоративные элементы */} -
-
-
-
- - {/* Брак */} - -
-
-
-
- -
- Брак -
- - -{Math.abs(warehouseStats.defectsTrend)}% - -
-
- {formatNumber(warehouseStats.currentDefects)} -
-
- Требует утилизации -
-
-
- - {/* Возвраты с ПВЗ */} - -
-
-
-
- -
- Возвраты с ПВЗ -
- - +{warehouseStats.returnsTrend}% - -
-
- {formatNumber(warehouseStats.currentReturns)} -
-
- К обработке -
-
-
- - {/* Расходники ФФ */} - -
-
-
-
- -
- Расходники ФФ -
-
-
- {formatNumber(warehouseStats.currentFulfillmentSupplies)} -
-
- Упаковка, этикетки, пленка -
-
-
- - {/* Расходники селлеров */} - -
-
-
-
- -
- Расходники селлеров -
-
-
- {formatNumber(warehouseStats.currentSellerSupplies)} -
-
- Материалы клиентов -
-
-
-
- )}
- +
+
+ {/* Основная скроллируемая часть - оставшиеся 70% экрана */} +
+
+ {/* Компактная шапка таблицы - максимум 10% экрана */} +
+
+

+ Детализация по магазинам +

+ + {filteredAndSortedStores.length} магазинов + +
+ {/* Компактный поиск */} +
+ + setSearchTerm(e.target.value)} + className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40" + /> +
+
+ + {/* Фиксированные заголовки таблицы */} +
+
+ + № / Магазин + + + Продукты + + + Товары + + + Брак + + + Расходники селлера + + + Возвраты с ПВЗ + +
+
+ + {/* Строка с суммами */} +
+
+
+ ИТОГО ({filteredAndSortedStores.length}) +
+
+
+ {formatNumber(totals.products)} +
+ {totals.productsChange >= 0 ? ( + + ) : ( + + )} + = 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {( + (totals.productsChange / totals.products) * + 100 + ).toFixed(1)} + % + +
+
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + +{Math.abs(Math.floor(totals.productsChange * 0.6))} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + -{Math.abs(Math.floor(totals.productsChange * 0.4))} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(totals.productsChange)} + +
+
+
+
+
+ {formatNumber(totals.goods)} +
+ {totals.goodsChange >= 0 ? ( + + ) : ( + + )} + = 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {((totals.goodsChange / totals.goods) * 100).toFixed(1)} + % + +
+
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + +{Math.abs(Math.floor(totals.goodsChange * 0.6))} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + -{Math.abs(Math.floor(totals.goodsChange * 0.4))} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(totals.goodsChange)} + +
+
+
+
+
+ {formatNumber(totals.defects)} +
+ {totals.defectsChange >= 0 ? ( + + ) : ( + + )} + = 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {( + (totals.defectsChange / totals.defects) * + 100 + ).toFixed(1)} + % + +
+
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + +{Math.abs(Math.floor(totals.defectsChange * 0.6))} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + -{Math.abs(Math.floor(totals.defectsChange * 0.4))} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(totals.defectsChange)} + +
+
+
+
+
+ {formatNumber(totals.sellerSupplies)} +
+ {totals.sellerSuppliesChange >= 0 ? ( + + ) : ( + + )} + = 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {( + (totals.sellerSuppliesChange / + totals.sellerSupplies) * + 100 + ).toFixed(1)} + % + +
+
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + + + {Math.abs( + Math.floor(totals.sellerSuppliesChange * 0.6) + )} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + - + {Math.abs( + Math.floor(totals.sellerSuppliesChange * 0.4) + )} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(totals.sellerSuppliesChange)} + +
+
+
+
+
+ {formatNumber(totals.pvzReturns)} +
+ {totals.pvzReturnsChange >= 0 ? ( + + ) : ( + + )} + = 0 + ? "text-green-400" + : "text-red-400" + }`} + > + {( + (totals.pvzReturnsChange / totals.pvzReturns) * + 100 + ).toFixed(1)} + % + +
+
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + +{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + -{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(totals.pvzReturnsChange)} + +
+
+
+
+
+ + {/* Скроллируемый контент таблицы - оставшееся пространство */} +
+ {filteredAndSortedStores.map((store, index) => ( +
+ {/* Основная строка магазина */} +
toggleStoreExpansion(store.id)} + > +
+ + {filteredAndSortedStores.length - index} + +
+
+ +
+
+
+ {store.name} +
+
+
+
+ +
+
+
+ {formatNumber(store.products)} +
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + + + {Math.abs(Math.floor(store.productsChange * 0.6))} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + - + {Math.abs(Math.floor(store.productsChange * 0.4))} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(store.productsChange)} + +
+
+
+
+ +
+
+
+ {formatNumber(store.goods)} +
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + +{Math.abs(Math.floor(store.goodsChange * 0.6))} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + -{Math.abs(Math.floor(store.goodsChange * 0.4))} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(store.goodsChange)} + +
+
+
+
+ +
+
+
+ {formatNumber(store.defects)} +
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + +{Math.abs(Math.floor(store.defectsChange * 0.6))} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + -{Math.abs(Math.floor(store.defectsChange * 0.4))} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(store.defectsChange)} + +
+
+
+
+ +
+
+
+ {formatNumber(store.sellerSupplies)} +
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + + + {Math.abs( + Math.floor(store.sellerSuppliesChange * 0.6) + )} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + - + {Math.abs( + Math.floor(store.sellerSuppliesChange * 0.4) + )} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(store.sellerSuppliesChange)} + +
+
+
+
+ +
+
+
+ {formatNumber(store.pvzReturns)} +
+
+ {/* Положительное изменение - всегда зеленое */} +
+ + + + {Math.abs( + Math.floor(store.pvzReturnsChange * 0.6) + )} + +
+ {/* Отрицательное изменение - всегда красное */} +
+ + - + {Math.abs( + Math.floor(store.pvzReturnsChange * 0.4) + )} + +
+ {/* Результирующее изменение */} +
+ + {Math.abs(store.pvzReturnsChange)} + +
+
+
+
+
+ + {/* Расширенная информация */} + {expandedStores.has(store.id) && ( +
+
+
+
+ + + Продукты + +
+
+ {formatNumber(store.products)} +
+
+ Готовые к отправке +
+
+ +
+
+ + + Товары + +
+
+ {formatNumber(store.goods)} +
+
+ В обработке +
+
+ +
+
+ + + Брак + +
+
+ {formatNumber(store.defects)} +
+
+ К утилизации +
+
+ +
+
+ + + Возвраты + +
+
+ {formatNumber(store.pvzReturns)} +
+
+ С ПВЗ +
+
+
+
+ )} +
+ ))} +
+
- ) -} \ No newline at end of file + ); +}