Files
sfera/src/components/supplies/marketplace-supplies/ozon-supplies-tab.tsx

771 lines
31 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 React, { useState } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { StatsCard } from "../ui/stats-card";
import { StatsGrid } from "../ui/stats-grid";
import {
ChevronDown,
ChevronRight,
Calendar,
Package,
MapPin,
TrendingUp,
AlertTriangle,
DollarSign,
Truck,
Store,
} from "lucide-react";
// Типы данных для поставок на Ozon
interface OzonProduct {
id: string;
name: string;
sku: string;
offerId: string;
category: string;
plannedQty: number;
actualQty: number;
defectQty: number;
productPrice: number;
}
interface OzonWarehouse {
id: string;
name: string;
address: string;
warehouseId: number;
products: OzonProduct[];
totalAmount: number;
}
interface OzonRoute {
id: string;
from: string;
fromAddress: string;
to: string;
toAddress: string;
warehouses: OzonWarehouse[];
totalProductPrice: number;
logisticsPrice: number;
totalAmount: number;
}
interface OzonSupply {
id: string;
number: number;
supplyId: string;
deliveryDate: string;
createdDate: string;
routes: OzonRoute[];
plannedTotal: number;
actualTotal: number;
defectTotal: number;
totalProductPrice: number;
totalLogisticsPrice: number;
grandTotal: number;
status: "planned" | "in-transit" | "delivered" | "completed";
}
// Моковые данные для поставок на Ozon
const mockOzonSupplies: OzonSupply[] = [
{
id: "oz1",
number: 5001,
supplyId: "OZ24010001",
deliveryDate: "2024-01-25",
createdDate: "2024-01-18",
status: "delivered",
plannedTotal: 90,
actualTotal: 87,
defectTotal: 3,
totalProductPrice: 1950000,
totalLogisticsPrice: 22000,
grandTotal: 1972000,
routes: [
{
id: "ozr1",
from: "ТЯК Москва",
fromAddress: "Москва, Алтуфьевское шоссе, 27",
to: "Ozon Тверь",
toAddress: "Тверь, ул. Складская, 45",
totalProductPrice: 1950000,
logisticsPrice: 22000,
totalAmount: 1972000,
warehouses: [
{
id: "ozw1",
name: "Склад Ozon Тверь",
address: "Тверь, ул. Складская, 45",
warehouseId: 22341172,
totalAmount: 1950000,
products: [
{
id: "ozp1",
name: "Ноутбук ASUS VivoBook",
sku: "ASUS-VB-15-512",
offerId: "ASUS-001",
category: "Ноутбуки",
plannedQty: 15,
actualQty: 14,
defectQty: 1,
productPrice: 85000,
},
{
id: "ozp2",
name: "Мышь беспроводная Logitech",
sku: "LOG-MX3-BLK",
offerId: "LOG-002",
category: "Компьютерные аксессуары",
plannedQty: 75,
actualQty: 73,
defectQty: 2,
productPrice: 4500,
},
],
},
],
},
],
},
{
id: "oz2",
number: 5002,
supplyId: "OZ24010002",
deliveryDate: "2024-01-30",
createdDate: "2024-01-22",
status: "in-transit",
plannedTotal: 45,
actualTotal: 45,
defectTotal: 0,
totalProductPrice: 1125000,
totalLogisticsPrice: 18000,
grandTotal: 1143000,
routes: [
{
id: "ozr2",
from: "Садовод",
fromAddress: "Москва, 14-й км МКАД",
to: "Ozon Рязань",
toAddress: "Рязань, ул. Промышленная, 15",
totalProductPrice: 1125000,
logisticsPrice: 18000,
totalAmount: 1143000,
warehouses: [
{
id: "ozw2",
name: "Склад Ozon Рязань",
address: "Рязань, ул. Промышленная, 15",
warehouseId: 22341173,
totalAmount: 1125000,
products: [
{
id: "ozp3",
name: "Планшет iPad Air",
sku: "APL-IPAD-AIR-64",
offerId: "APL-003",
category: "Планшеты",
plannedQty: 20,
actualQty: 20,
defectQty: 0,
productPrice: 45000,
},
{
id: "ozp4",
name: "Клавиатура механическая",
sku: "KEYB-MECH-RGB",
offerId: "KEYB-004",
category: "Клавиатуры",
plannedQty: 25,
actualQty: 25,
defectQty: 0,
productPrice: 12000,
},
],
},
],
},
],
},
];
export function OzonSuppliesTab() {
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
new Set()
);
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set());
const [expandedWarehouses, setExpandedWarehouses] = useState<Set<string>>(
new Set()
);
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(
new Set()
);
const toggleSupplyExpansion = (supplyId: string) => {
const newExpanded = new Set(expandedSupplies);
if (newExpanded.has(supplyId)) {
newExpanded.delete(supplyId);
} else {
newExpanded.add(supplyId);
}
setExpandedSupplies(newExpanded);
};
const toggleRouteExpansion = (routeId: string) => {
const newExpanded = new Set(expandedRoutes);
if (newExpanded.has(routeId)) {
newExpanded.delete(routeId);
} else {
newExpanded.add(routeId);
}
setExpandedRoutes(newExpanded);
};
const toggleWarehouseExpansion = (warehouseId: string) => {
const newExpanded = new Set(expandedWarehouses);
if (newExpanded.has(warehouseId)) {
newExpanded.delete(warehouseId);
} else {
newExpanded.add(warehouseId);
}
setExpandedWarehouses(newExpanded);
};
const toggleProductExpansion = (productId: string) => {
const newExpanded = new Set(expandedProducts);
if (newExpanded.has(productId)) {
newExpanded.delete(productId);
} else {
newExpanded.add(productId);
}
setExpandedProducts(newExpanded);
};
const getStatusBadge = (status: OzonSupply["status"]) => {
const statusMap = {
planned: {
label: "Запланирована",
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
},
"in-transit": {
label: "В пути",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
},
delivered: {
label: "Доставлена",
color: "bg-green-500/20 text-green-300 border-green-500/30",
},
completed: {
label: "Завершена",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
},
};
const { label, color } = statusMap[status];
return <Badge className={`${color} border`}>{label}</Badge>;
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
}).format(amount);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
const calculateProductTotal = (product: OzonProduct) => {
return product.actualQty * product.productPrice;
};
const getEfficiencyBadge = (
planned: number,
actual: number,
defect: number
) => {
const efficiency = ((actual - defect) / planned) * 100;
if (efficiency >= 95) {
return (
<Badge className="bg-green-500/20 text-green-300 border-green-500/30 border">
Отлично
</Badge>
);
} else if (efficiency >= 90) {
return (
<Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-500/30 border">
Хорошо
</Badge>
);
} else {
return (
<Badge className="bg-red-500/20 text-red-300 border-red-500/30 border">
Проблемы
</Badge>
);
}
};
return (
<div className="space-y-6">
{/* Статистика поставок на Ozon */}
<StatsGrid>
<StatsCard
title="Поставок на Ozon"
value={mockOzonSupplies.length}
icon={Store}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
trend={{ value: 14, isPositive: true }}
subtitle="Ozon поставки"
/>
<StatsCard
title="Сумма Ozon поставок"
value={formatCurrency(
mockOzonSupplies.reduce((sum, supply) => sum + supply.grandTotal, 0)
)}
icon={TrendingUp}
iconColor="text-green-400"
iconBg="bg-green-500/20"
trend={{ value: 19, isPositive: true }}
subtitle="Общая стоимость"
/>
<StatsCard
title="В пути"
value={
mockOzonSupplies.filter((supply) => supply.status === "in-transit")
.length
}
icon={Calendar}
iconColor="text-yellow-400"
iconBg="bg-yellow-500/20"
subtitle="Активные поставки"
/>
<StatsCard
title="С браком"
value={
mockOzonSupplies.filter((supply) => supply.defectTotal > 0).length
}
icon={AlertTriangle}
iconColor="text-red-400"
iconBg="bg-red-500/20"
trend={{ value: 2, isPositive: false }}
subtitle="Требуют проверки"
/>
</StatsGrid>
{/* Таблица поставок на Ozon */}
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-4 text-white font-semibold"></th>
<th className="text-left p-4 text-white font-semibold">
ID поставки
</th>
<th className="text-left p-4 text-white font-semibold">
Дата поставки
</th>
<th className="text-left p-4 text-white font-semibold">
Дата создания
</th>
<th className="text-left p-4 text-white font-semibold">План</th>
<th className="text-left p-4 text-white font-semibold">Факт</th>
<th className="text-left p-4 text-white font-semibold">Брак</th>
<th className="text-left p-4 text-white font-semibold">
Цена товаров
</th>
<th className="text-left p-4 text-white font-semibold">
Логистика
</th>
<th className="text-left p-4 text-white font-semibold">
Итого сумма
</th>
<th className="text-left p-4 text-white font-semibold">
Статус
</th>
</tr>
</thead>
<tbody>
{mockOzonSupplies.map((supply) => {
const isSupplyExpanded = expandedSupplies.has(supply.id);
return (
<React.Fragment key={supply.id}>
{/* Основная строка поставки на Ozon */}
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-blue-500/10 cursor-pointer"
onClick={() => toggleSupplyExpansion(supply.id)}
>
<td className="p-4">
<div className="flex items-center space-x-2">
<span className="text-white font-normal text-lg">
{supply.number}
</span>
</div>
</td>
<td className="p-4">
<span className="text-blue-300 font-mono text-sm">
{supply.supplyId}
</span>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-white/40" />
<span className="text-white font-semibold">
{formatDate(supply.deliveryDate)}
</span>
</div>
</td>
<td className="p-4">
<span className="text-white/80">
{formatDate(supply.createdDate)}
</span>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{supply.plannedTotal}
</span>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{supply.actualTotal}
</span>
</td>
<td className="p-4">
<span
className={`font-semibold ${
supply.defectTotal > 0
? "text-red-400"
: "text-white"
}`}
>
{supply.defectTotal}
</span>
</td>
<td className="p-4">
<span className="text-green-400 font-semibold">
{formatCurrency(supply.totalProductPrice)}
</span>
</td>
<td className="p-4">
<span className="text-purple-400 font-semibold">
{formatCurrency(supply.totalLogisticsPrice)}
</span>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
<DollarSign className="h-4 w-4 text-white/40" />
<span className="text-white font-bold text-lg">
{formatCurrency(supply.grandTotal)}
</span>
</div>
</td>
<td className="p-4">{getStatusBadge(supply.status)}</td>
</tr>
{/* Развернутые уровни */}
{isSupplyExpanded &&
supply.routes.map((route) => {
const isRouteExpanded = expandedRoutes.has(route.id);
return (
<React.Fragment key={route.id}>
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors bg-cyan-500/10">
<td className="p-4 pl-12">
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
toggleRouteExpansion(route.id)
}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
{isRouteExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
<MapPin className="h-4 w-4 text-cyan-400" />
<span className="text-white font-medium">
Маршрут
</span>
</div>
</td>
<td className="p-4" colSpan={3}>
<div className="text-white">
<div className="flex items-center space-x-2 mb-1">
<span className="font-medium">
{route.from}
</span>
<span className="text-white/60"></span>
<span className="font-medium">
{route.to}
</span>
</div>
<div className="text-xs text-white/60">
{route.fromAddress} {route.toAddress}
</div>
</div>
</td>
<td className="p-4">
<span className="text-white/80">
{route.warehouses.reduce(
(sum, w) =>
sum +
w.products.reduce(
(pSum, p) => pSum + p.plannedQty,
0
),
0
)}
</span>
</td>
<td className="p-4">
<span className="text-white/80">
{route.warehouses.reduce(
(sum, w) =>
sum +
w.products.reduce(
(pSum, p) => pSum + p.actualQty,
0
),
0
)}
</span>
</td>
<td className="p-4">
<span className="text-white/80">
{route.warehouses.reduce(
(sum, w) =>
sum +
w.products.reduce(
(pSum, p) => pSum + p.defectQty,
0
),
0
)}
</span>
</td>
<td className="p-4">
<span className="text-green-400 font-medium">
{formatCurrency(route.totalProductPrice)}
</span>
</td>
<td className="p-4">
<span className="text-purple-400 font-medium">
{formatCurrency(route.logisticsPrice)}
</span>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{formatCurrency(route.totalAmount)}
</span>
</td>
<td className="p-4"></td>
</tr>
{/* Склады Ozon */}
{isRouteExpanded &&
route.warehouses.map((warehouse) => {
const isWarehouseExpanded =
expandedWarehouses.has(warehouse.id);
return (
<React.Fragment key={warehouse.id}>
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors bg-indigo-500/10">
<td className="p-4 pl-20">
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
toggleWarehouseExpansion(
warehouse.id
)
}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
{isWarehouseExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
<Truck className="h-4 w-4 text-indigo-400" />
<span className="text-white font-medium">
Склад Ozon
</span>
</div>
</td>
<td className="p-4" colSpan={3}>
<div className="text-white">
<div className="font-medium mb-1">
{warehouse.name}
</div>
<div className="text-xs text-white/60 mb-1">
ID: {warehouse.warehouseId}
</div>
<div className="text-xs text-white/60">
{warehouse.address}
</div>
</div>
</td>
<td className="p-4">
<span className="text-white/80">
{warehouse.products.reduce(
(sum, p) => sum + p.plannedQty,
0
)}
</span>
</td>
<td className="p-4">
<span className="text-white/80">
{warehouse.products.reduce(
(sum, p) => sum + p.actualQty,
0
)}
</span>
</td>
<td className="p-4">
<span className="text-white/80">
{warehouse.products.reduce(
(sum, p) => sum + p.defectQty,
0
)}
</span>
</td>
<td className="p-4">
<span className="text-green-400 font-medium">
{formatCurrency(
warehouse.products.reduce(
(sum, p) =>
sum + calculateProductTotal(p),
0
)
)}
</span>
</td>
<td className="p-4"></td>
<td className="p-4">
<span className="text-white font-semibold">
{formatCurrency(
warehouse.totalAmount
)}
</span>
</td>
<td className="p-4"></td>
</tr>
{/* Товары Ozon */}
{isWarehouseExpanded &&
warehouse.products.map((product) => (
<tr
key={product.id}
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-teal-500/10"
>
<td className="p-4 pl-28">
<div className="flex items-center space-x-2">
<Package className="h-4 w-4 text-teal-400" />
<span className="text-white font-medium">
Товар Ozon
</span>
</div>
</td>
<td className="p-4" colSpan={3}>
<div className="text-white">
<div className="font-medium mb-1">
{product.name}
</div>
<div className="text-xs text-white/60 mb-1">
Артикул: {product.sku}
</div>
<div className="text-xs text-white/60 mb-1">
Offer ID: {product.offerId}
</div>
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs">
{product.category}
</Badge>
</div>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{product.plannedQty}
</span>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{product.actualQty}
</span>
</td>
<td className="p-4">
<span
className={`font-semibold ${
product.defectQty > 0
? "text-red-400"
: "text-white"
}`}
>
{product.defectQty}
</span>
</td>
<td className="p-4">
<div className="text-white">
<div className="font-medium">
{formatCurrency(
calculateProductTotal(product)
)}
</div>
<div className="text-xs text-white/60">
{formatCurrency(
product.productPrice
)}{" "}
за шт.
</div>
</div>
</td>
<td className="p-4">
{getEfficiencyBadge(
product.plannedQty,
product.actualQty,
product.defectQty
)}
</td>
<td className="p-4">
<span className="text-white font-semibold">
{formatCurrency(
calculateProductTotal(product)
)}
</span>
</td>
<td className="p-4"></td>
</tr>
))}
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
</Card>
</div>
);
}