Добавлен новый демо-компонент FulfillmentWarehouse2Demo в интерфейс управления складами. Обновлен компонент UIKitSection для интеграции нового демо и изменения текстовых меток. Оптимизирован интерфейс с использованием новых компонентов и улучшена логика отображения данных о складах.
This commit is contained in:
@ -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>
|
||||
|
699
src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx
Normal file
699
src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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<keyof StoreData>("name");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [expandedStores, setExpandedStores] = useState<Set<string>>(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
|
||||
}) => (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user