Files
sfera-new/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx

701 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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: 0, change: 0 }, // Нет данных о продуктах
goods: { current: 0, change: 0 }, // Нет данных о товарах
defects: { current: 0, change: 0 }, // Нет данных о браке
pvzReturns: { current: 0, change: 0 }, // Нет данных о возвратах с ПВЗ
fulfillmentSupplies: { current: 0, change: 0 }, // Нет данных о расходниках ФФ
sellerSupplies: { current: 0, change: 0 }, // Нет данных о расходниках селлера
};
// Мок данные для магазинов
const mockStoreData: StoreData[] = useMemo(
() => [
{
id: "1",
name: "Электроника Плюс",
products: 456,
goods: 234,
defects: 12,
sellerSupplies: 89,
pvzReturns: 45,
productsChange: 23,
goodsChange: 15,
defectsChange: -3,
sellerSuppliesChange: 12,
pvzReturnsChange: 8,
},
{
id: "2",
name: "Мода и Стиль",
products: 678,
goods: 345,
defects: 8,
sellerSupplies: 123,
pvzReturns: 67,
productsChange: 34,
goodsChange: 22,
defectsChange: -2,
sellerSuppliesChange: 18,
pvzReturnsChange: 12,
},
{
id: "3",
name: "Дом и Сад",
products: 289,
goods: 156,
defects: 5,
sellerSupplies: 67,
pvzReturns: 23,
productsChange: 12,
goodsChange: 8,
defectsChange: -1,
sellerSuppliesChange: 9,
pvzReturnsChange: 4,
},
{
id: "4",
name: "Спорт и Отдых",
products: 567,
goods: 289,
defects: 15,
sellerSupplies: 134,
pvzReturns: 78,
productsChange: 28,
goodsChange: 19,
defectsChange: -4,
sellerSuppliesChange: 21,
pvzReturnsChange: 15,
},
{
id: "5",
name: "Красота и Здоровье",
products: 234,
goods: 123,
defects: 3,
sellerSupplies: 45,
pvzReturns: 19,
productsChange: 8,
goodsChange: 5,
defectsChange: 0,
sellerSuppliesChange: 6,
pvzReturnsChange: 3,
},
],
[]
);
// Фильтрация и сортировка данных
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>
);
}