Files
sfera-new/src/components/supplies/marketplace-supplies/wildberries-supplies-tab.tsx

668 lines
28 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 { Badge } from "@/components/ui/badge";
import { StatsCard } from "../ui/stats-card";
import { StatsGrid } from "../ui/stats-grid";
import { useQuery } from "@apollo/client";
import { GET_MY_WILDBERRIES_SUPPLIES } from "@/graphql/queries";
import {
Calendar,
Package,
MapPin,
TrendingUp,
AlertTriangle,
DollarSign,
Truck,
ShoppingBag,
} from "lucide-react";
// Типы данных для поставок на Wildberries
interface WbProduct {
id: string;
name: string;
sku: string;
nmId: number;
category: string;
plannedQty: number;
actualQty: number;
defectQty: number;
productPrice: number;
}
interface WbWarehouse {
id: string;
name: string;
address: string;
warehouseId: number;
products: WbProduct[];
totalAmount: number;
}
interface WbRoute {
id: string;
from: string;
fromAddress: string;
to: string;
toAddress: string;
warehouses: WbWarehouse[];
totalProductPrice: number;
logisticsPrice: number;
totalAmount: number;
}
interface WbSupply {
id: string;
number: number;
supplyId: string;
deliveryDate: string;
createdDate: string;
routes: WbRoute[];
plannedTotal: number;
actualTotal: number;
defectTotal: number;
totalProductPrice: number;
totalLogisticsPrice: number;
grandTotal: number;
status: "planned" | "in-transit" | "delivered" | "completed";
}
export function WildberriesSuppliesTab() {
// Загружаем реальные данные поставок на Wildberries
const { data: wbSuppliesData, loading, error } = useQuery(GET_MY_WILDBERRIES_SUPPLIES, {
errorPolicy: 'all'
});
// Преобразуем данные из GraphQL в нужный формат
const wbSupplies: WbSupply[] = (wbSuppliesData?.myWildberriesSupplies || [])
.map((supply: any, index: number) => ({
id: supply.id,
number: index + 4000, // Начинаем с 4000 для WB поставок
supplyId: `WB${new Date().getFullYear()}${String(index + 1).padStart(6, '0')}`,
deliveryDate: supply.deliveryDate || new Date().toISOString().split('T')[0],
createdDate: supply.createdAt?.split('T')[0] || new Date().toISOString().split('T')[0],
status: supply.status === 'DELIVERED' ? 'delivered' : 'in-transit',
plannedTotal: supply.totalItems || 0,
actualTotal: supply.totalItems || 0,
defectTotal: 0,
totalProductPrice: supply.totalAmount || 0,
totalLogisticsPrice: 0,
grandTotal: supply.totalAmount || 0,
routes: []
}));
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: WbSupply["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: WbProduct) => {
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">
{/* Статистика поставок на Wildberries */}
<StatsGrid>
<StatsCard
title="Поставок на WB"
value={loading ? 0 : wbSupplies.length}
icon={ShoppingBag}
iconColor="text-purple-400"
iconBg="bg-purple-500/20"
trend={{ value: 18, isPositive: true }}
subtitle="Wildberries поставки"
/>
<StatsCard
title="Сумма WB поставок"
value={formatCurrency(
loading ? 0 : wbSupplies.reduce((sum, supply) => sum + supply.grandTotal, 0)
)}
icon={TrendingUp}
iconColor="text-green-400"
iconBg="bg-green-500/20"
trend={{ value: 22, isPositive: true }}
subtitle="Общая стоимость"
/>
<StatsCard
title="В пути"
value={
loading ? 0 : wbSupplies.filter((supply) => supply.status === "in-transit")
.length
}
icon={Calendar}
iconColor="text-yellow-400"
iconBg="bg-yellow-500/20"
subtitle="Активные поставки"
/>
<StatsCard
title="С браком"
value={
loading ? 0 : wbSupplies.filter((supply) => supply.defectTotal > 0).length
}
icon={AlertTriangle}
iconColor="text-red-400"
iconBg="bg-red-500/20"
trend={{ value: 1, isPositive: false }}
subtitle="Требуют проверки"
/>
</StatsGrid>
{/* Таблица поставок на Wildberries */}
<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>
{loading && (
<tr>
<td colSpan={11} className="p-8 text-center">
<div className="text-white/60">Загрузка данных...</div>
</td>
</tr>
)}
{!loading && wbSupplies.length === 0 && (
<tr>
<td colSpan={11} className="p-8 text-center">
<div className="text-white/60">
<ShoppingBag className="h-12 w-12 mx-auto mb-4 text-white/20" />
<div className="text-lg font-semibold text-white mb-2">Поставки на Wildberries не найдены</div>
<div>Создайте первую поставку товаров на Wildberries</div>
</div>
</td>
</tr>
)}
{!loading && wbSupplies.map((supply) => {
const isSupplyExpanded = expandedSupplies.has(supply.id);
return (
<React.Fragment key={supply.id}>
{/* Основная строка поставки на WB */}
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-purple-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-purple-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-blue-500/10 cursor-pointer"
onClick={() => toggleRouteExpansion(route.id)}
>
<td className="p-4">
<div className="flex items-center space-x-2">
<MapPin className="h-4 w-4 text-blue-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>
{/* Склады WB */}
{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-green-500/10 cursor-pointer"
onClick={() =>
toggleWarehouseExpansion(warehouse.id)
}
>
<td className="p-4">
<div className="flex items-center space-x-2">
<Truck className="h-4 w-4 text-green-400" />
<span className="text-white font-medium">
Склад WB
</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>
{/* Товары WB */}
{isWarehouseExpanded &&
warehouse.products.map((product) => (
<tr
key={product.id}
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-yellow-500/10"
>
<td className="p-4">
<div className="flex items-center space-x-2">
<Package className="h-4 w-4 text-yellow-400" />
<span className="text-white font-medium">
Товар WB
</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">
NM ID: {product.nmId}
</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>
);
}