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

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

View File

@ -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>
);
}