Files
sfera-new/src/components/supplies/fulfillment-supplies/fulfillment-goods-tab.tsx

836 lines
37 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 {
Calendar,
Package,
MapPin,
Building2,
TrendingUp,
AlertTriangle,
DollarSign,
Warehouse,
} from "lucide-react";
// Типы данных для товаров ФФ
interface ProductParameter {
id: string;
name: string;
value: string;
unit?: string;
}
interface Product {
id: string;
name: string;
sku: string;
category: string;
plannedQty: number;
actualQty: number;
defectQty: number;
productPrice: number;
parameters: ProductParameter[];
}
interface Wholesaler {
id: string;
name: string;
inn: string;
contact: string;
address: string;
products: Product[];
totalAmount: number;
}
interface Route {
id: string;
from: string;
fromAddress: string;
to: string;
toAddress: string;
wholesalers: Wholesaler[];
totalProductPrice: number;
fulfillmentServicePrice: number;
logisticsPrice: number;
totalAmount: number;
}
interface FulfillmentSupply {
id: string;
number: number;
deliveryDate: string;
createdDate: string;
routes: Route[];
plannedTotal: number;
actualTotal: number;
defectTotal: number;
totalProductPrice: number;
totalFulfillmentPrice: number;
totalLogisticsPrice: number;
grandTotal: number;
status: "planned" | "in-transit" | "delivered" | "completed";
}
// Моковые данные для товаров ФФ
const mockFulfillmentGoods: FulfillmentSupply[] = [
{
id: "ff1",
number: 1001,
deliveryDate: "2024-01-15",
createdDate: "2024-01-10",
status: "delivered",
plannedTotal: 180,
actualTotal: 173,
defectTotal: 2,
totalProductPrice: 3750000,
totalFulfillmentPrice: 43000,
totalLogisticsPrice: 27000,
grandTotal: 3820000,
routes: [
{
id: "ffr1",
from: "Садовод",
fromAddress: "Москва, 14-й км МКАД",
to: "SFERAV Logistics ФФ",
toAddress: "Москва, ул. Складская, 15",
totalProductPrice: 3600000,
fulfillmentServicePrice: 25000,
logisticsPrice: 15000,
totalAmount: 3640000,
wholesalers: [
{
id: "ffw1",
name: 'ООО "ТехноСнаб"',
inn: "7701234567",
contact: "+7 (495) 123-45-67",
address: "Москва, ул. Торговая, 1",
totalAmount: 3600000,
products: [
{
id: "ffp1",
name: "Смартфон iPhone 15 Pro",
sku: "APL-IP15P-256",
category: "Электроника",
plannedQty: 50,
actualQty: 48,
defectQty: 2,
productPrice: 75000,
parameters: [
{ id: "param1", name: "Цвет", value: "Титановый" },
{ id: "param2", name: "Память", value: "256", unit: "ГБ" },
{ id: "param3", name: "Гарантия", value: "12", unit: "мес" },
],
},
],
},
],
},
],
},
{
id: "ff2",
number: 1002,
deliveryDate: "2024-01-20",
createdDate: "2024-01-12",
status: "in-transit",
plannedTotal: 30,
actualTotal: 30,
defectTotal: 0,
totalProductPrice: 750000,
totalFulfillmentPrice: 18000,
totalLogisticsPrice: 12000,
grandTotal: 780000,
routes: [
{
id: "ffr2",
from: "Садовод",
fromAddress: "Москва, 14-й км МКАД",
to: "MegaFulfillment",
toAddress: "Подольск, ул. Складская, 25",
totalProductPrice: 750000,
fulfillmentServicePrice: 18000,
logisticsPrice: 12000,
totalAmount: 780000,
wholesalers: [
{
id: "ffw2",
name: 'ООО "АудиоТех"',
inn: "7702345678",
contact: "+7 (495) 555-12-34",
address: "Москва, ул. Звуковая, 8",
totalAmount: 750000,
products: [
{
id: "ffp2",
name: "Наушники AirPods Pro 2",
sku: "APL-AP-PRO2-USB",
category: "Аудио",
plannedQty: 30,
actualQty: 30,
defectQty: 0,
productPrice: 25000,
parameters: [
{ id: "param4", name: "Тип", value: "Беспроводные" },
{ id: "param5", name: "Шумоподавление", value: "Активное" },
{ id: "param6", name: "Время работы", value: "6", unit: "ч" },
],
},
],
},
],
},
],
},
];
export function FulfillmentGoodsTab() {
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
new Set()
);
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set());
const [expandedWholesalers, setExpandedWholesalers] = 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 toggleWholesalerExpansion = (wholesalerId: string) => {
const newExpanded = new Set(expandedWholesalers);
if (newExpanded.has(wholesalerId)) {
newExpanded.delete(wholesalerId);
} else {
newExpanded.add(wholesalerId);
}
setExpandedWholesalers(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: FulfillmentSupply["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: Product) => {
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="h-full flex flex-col space-y-6">
{/* Статистика товаров ФФ */}
<StatsGrid>
<StatsCard
title="Поставок товаров ФФ"
value={mockFulfillmentGoods.length}
icon={Warehouse}
iconColor="text-purple-400"
iconBg="bg-purple-500/20"
trend={{ value: 12, isPositive: true }}
subtitle="За текущий месяц"
/>
<StatsCard
title="Сумма товаров ФФ"
value={formatCurrency(
mockFulfillmentGoods.reduce(
(sum, supply) => sum + supply.grandTotal,
0
)
)}
icon={TrendingUp}
iconColor="text-green-400"
iconBg="bg-green-500/20"
trend={{ value: 8, isPositive: true }}
subtitle="Общая стоимость"
/>
<StatsCard
title="В пути"
value={
mockFulfillmentGoods.filter(
(supply) => supply.status === "in-transit"
).length
}
icon={Calendar}
iconColor="text-yellow-400"
iconBg="bg-yellow-500/20"
subtitle="Активные поставки"
/>
<StatsCard
title="С браком"
value={
mockFulfillmentGoods.filter((supply) => supply.defectTotal > 0)
.length
}
icon={AlertTriangle}
iconColor="text-red-400"
iconBg="bg-red-500/20"
trend={{ value: 3, isPositive: false }}
subtitle="Требуют внимания"
/>
</StatsGrid>
{/* Таблица поставок товаров ФФ */}
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden flex-1 flex flex-col">
<div className="overflow-auto flex-1">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-2 text-white font-semibold text-sm">
</th>
<th className="text-left p-2 text-white font-semibold text-sm">
<span className="hidden sm:inline">Дата поставки</span>
<span className="sm:hidden">Поставка</span>
</th>
<th className="text-left p-2 text-white font-semibold text-sm hidden lg:table-cell">
Создана
</th>
<th className="text-left p-2 text-white font-semibold text-sm">
План
</th>
<th className="text-left p-2 text-white font-semibold text-sm">
Факт
</th>
<th className="text-left p-2 text-white font-semibold text-sm">
Брак
</th>
<th className="text-left p-2 text-white font-semibold text-sm">
<span className="hidden md:inline">Цена товаров</span>
<span className="md:hidden">Цена</span>
</th>
<th className="text-left p-2 text-white font-semibold text-sm hidden lg:table-cell">
ФФ
</th>
<th className="text-left p-2 text-white font-semibold text-sm hidden lg:table-cell">
Логистика
</th>
<th className="text-left p-2 text-white font-semibold text-sm">
Итого
</th>
<th className="text-left p-2 text-white font-semibold text-sm">
Статус
</th>
</tr>
</thead>
<tbody>
{mockFulfillmentGoods.map((supply) => {
const isSupplyExpanded = expandedSupplies.has(supply.id);
return (
<React.Fragment key={supply.id}>
{/* Основная строка поставки */}
<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-2">
<span className="text-white font-normal text-sm">
{supply.number}
</span>
</td>
<td className="p-2">
<div className="flex items-center space-x-1">
<Calendar className="h-3 w-3 text-white/40" />
<span className="text-white font-semibold text-sm">
{formatDate(supply.deliveryDate)}
</span>
</div>
</td>
<td className="p-2 hidden lg:table-cell">
<span className="text-white/80 text-sm">
{formatDate(supply.createdDate)}
</span>
</td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{supply.plannedTotal}
</span>
</td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{supply.actualTotal}
</span>
</td>
<td className="p-2">
<span
className={`font-semibold text-sm ${
supply.defectTotal > 0
? "text-red-400"
: "text-white"
}`}
>
{supply.defectTotal}
</span>
</td>
<td className="p-2">
<span className="text-green-400 font-semibold text-sm">
{formatCurrency(supply.totalProductPrice)}
</span>
</td>
<td className="p-2 hidden lg:table-cell">
<span className="text-blue-400 font-semibold text-sm">
{formatCurrency(supply.totalFulfillmentPrice)}
</span>
</td>
<td className="p-2 hidden lg:table-cell">
<span className="text-purple-400 font-semibold text-sm">
{formatCurrency(supply.totalLogisticsPrice)}
</span>
</td>
<td className="p-2">
<div className="flex items-center space-x-1">
<DollarSign className="h-3 w-3 text-white/40" />
<span className="text-white font-bold text-sm">
{formatCurrency(supply.grandTotal)}
</span>
</div>
</td>
<td className="p-2">{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-2 relative">
<div className="flex items-center space-x-2">
<div className="w-1 h-1 rounded-full bg-blue-400 mr-1"></div>
<MapPin className="h-3 w-3 text-blue-400" />
<span className="text-white font-medium text-sm">
Маршрут
</span>
</div>
<div className="absolute left-0 top-0 w-0.5 h-full bg-blue-400/30"></div>
</td>
<td className="p-2" colSpan={1}>
<div className="text-white">
<div className="flex items-center space-x-2 mb-1">
<span className="font-medium text-sm">
{route.from}
</span>
<span className="text-white/60"></span>
<span className="font-medium text-sm">
{route.to}
</span>
</div>
<div className="text-xs text-white/60 hidden sm:block">
{route.fromAddress} {route.toAddress}
</div>
</div>
</td>
<td className="p-2 hidden lg:table-cell"></td>
<td className="p-2">
<span className="text-white/80 text-sm">
{route.wholesalers.reduce(
(sum, w) =>
sum +
w.products.reduce(
(pSum, p) => pSum + p.plannedQty,
0
),
0
)}
</span>
</td>
<td className="p-2">
<span className="text-white/80 text-sm">
{route.wholesalers.reduce(
(sum, w) =>
sum +
w.products.reduce(
(pSum, p) => pSum + p.actualQty,
0
),
0
)}
</span>
</td>
<td className="p-2">
<span className="text-white/80 text-sm">
{route.wholesalers.reduce(
(sum, w) =>
sum +
w.products.reduce(
(pSum, p) => pSum + p.defectQty,
0
),
0
)}
</span>
</td>
<td className="p-2">
<span className="text-green-400 font-medium text-sm">
{formatCurrency(route.totalProductPrice)}
</span>
</td>
<td className="p-2 hidden lg:table-cell">
<span className="text-blue-400 font-medium text-sm">
{formatCurrency(
route.fulfillmentServicePrice
)}
</span>
</td>
<td className="p-2 hidden lg:table-cell">
<span className="text-purple-400 font-medium text-sm">
{formatCurrency(route.logisticsPrice)}
</span>
</td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{formatCurrency(route.totalAmount)}
</span>
</td>
<td className="p-2"></td>
</tr>
{/* Остальные уровни развертывания аналогично */}
{isRouteExpanded &&
route.wholesalers.map((wholesaler) => {
const isWholesalerExpanded =
expandedWholesalers.has(wholesaler.id);
return (
<React.Fragment key={wholesaler.id}>
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-green-500/10 cursor-pointer"
onClick={() =>
toggleWholesalerExpansion(wholesaler.id)
}
>
<td className="p-2 relative">
<div className="flex items-center space-x-2">
<div className="w-1 h-1 rounded-full bg-green-400 mr-1"></div>
<div className="w-1 h-1 rounded-full bg-green-400 mr-1"></div>
<Building2 className="h-3 w-3 text-green-400" />
<span className="text-white font-medium text-sm">
Поставщик
</span>
</div>
<div className="absolute left-0 top-0 w-0.5 h-full bg-green-400/30"></div>
</td>
<td className="p-2" colSpan={1}>
<div className="text-white">
<div className="font-medium mb-1 text-sm">
{wholesaler.name}
</div>
<div className="text-xs text-white/60 mb-1 hidden sm:block">
ИНН: {wholesaler.inn}
</div>
<div className="text-xs text-white/60 mb-1 hidden lg:block">
{wholesaler.address}
</div>
<div className="text-xs text-white/60 hidden sm:block">
{wholesaler.contact}
</div>
</div>
</td>
<td className="p-2 hidden lg:table-cell"></td>
<td className="p-2">
<span className="text-white/80 text-sm">
{wholesaler.products.reduce(
(sum, p) => sum + p.plannedQty,
0
)}
</span>
</td>
<td className="p-2">
<span className="text-white/80 text-sm">
{wholesaler.products.reduce(
(sum, p) => sum + p.actualQty,
0
)}
</span>
</td>
<td className="p-2">
<span className="text-white/80 text-sm">
{wholesaler.products.reduce(
(sum, p) => sum + p.defectQty,
0
)}
</span>
</td>
<td className="p-2">
<span className="text-green-400 font-medium text-sm">
{formatCurrency(
wholesaler.products.reduce(
(sum, p) =>
sum + calculateProductTotal(p),
0
)
)}
</span>
</td>
<td
className="p-2 hidden lg:table-cell"
colSpan={2}
></td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{formatCurrency(
wholesaler.totalAmount
)}
</span>
</td>
<td className="p-4"></td>
</tr>
{/* Товары */}
{isWholesalerExpanded &&
wholesaler.products.map((product) => {
const isProductExpanded =
expandedProducts.has(product.id);
return (
<React.Fragment key={product.id}>
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-yellow-500/10 cursor-pointer"
onClick={() =>
toggleProductExpansion(
product.id
)
}
>
<td className="p-2 relative">
<div className="flex items-center space-x-2">
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
<Package className="h-3 w-3 text-yellow-400" />
<span className="text-white font-medium text-sm">
Товар
</span>
</div>
<div className="absolute left-0 top-0 w-0.5 h-full bg-yellow-400/30"></div>
</td>
<td className="p-2" colSpan={1}>
<div className="text-white">
<div className="font-medium mb-1 text-sm">
{product.name}
</div>
<div className="text-xs text-white/60 mb-1 hidden sm:block">
Артикул: {product.sku}
</div>
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs hidden sm:inline-flex">
{product.category}
</Badge>
</div>
</td>
<td className="p-2 hidden lg:table-cell"></td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{product.plannedQty}
</span>
</td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{product.actualQty}
</span>
</td>
<td className="p-2">
<span
className={`font-semibold text-sm ${
product.defectQty > 0
? "text-red-400"
: "text-white"
}`}
>
{product.defectQty}
</span>
</td>
<td className="p-2">
<div className="text-white">
<div className="font-medium text-sm">
{formatCurrency(
calculateProductTotal(
product
)
)}
</div>
<div className="text-xs text-white/60 hidden sm:block">
{formatCurrency(
product.productPrice
)}{" "}
за шт.
</div>
</div>
</td>
<td
className="p-2 hidden lg:table-cell"
colSpan={2}
>
{getEfficiencyBadge(
product.plannedQty,
product.actualQty,
product.defectQty
)}
</td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{formatCurrency(
calculateProductTotal(
product
)
)}
</span>
</td>
<td className="p-2"></td>
</tr>
{/* Параметры товара */}
{isProductExpanded && (
<tr>
<td
colSpan={11}
className="p-0"
>
<div className="bg-white/5 border-t border-white/10">
<div className="p-4">
<h4 className="text-white font-medium mb-3 flex items-center space-x-2">
<span className="text-xs text-white/60">
📋 Параметры товара:
</span>
</h4>
<div className="grid grid-cols-3 gap-4">
{product.parameters.map(
(param) => (
<div
key={param.id}
className="bg-white/5 rounded-lg p-3"
>
<div className="text-white/80 text-xs font-medium mb-1">
{param.name}
</div>
<div className="text-white text-sm">
{param.value}{" "}
{param.unit ||
""}
</div>
</div>
)
)}
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
</Card>
</div>
);
}