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

844 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 { 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,
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">
<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">
Дата поставки
</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>
{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-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">
<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-blue-400 font-semibold">
{formatCurrency(supply.totalFulfillmentPrice)}
</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">
<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-blue-400" />
<span className="text-white font-medium">
Маршрут
</span>
</div>
</td>
<td className="p-4" colSpan={2}>
<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.wholesalers.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.wholesalers.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.wholesalers.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-blue-400 font-medium">
{formatCurrency(
route.fulfillmentServicePrice
)}
</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>
{/* Остальные уровни развертывания аналогично */}
{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">
<td className="p-4 pl-20">
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
toggleWholesalerExpansion(
wholesaler.id
)
}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
{isWholesalerExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
<Building2 className="h-4 w-4 text-green-400" />
<span className="text-white font-medium">
Оптовик
</span>
</div>
</td>
<td className="p-4" colSpan={2}>
<div className="text-white">
<div className="font-medium mb-1">
{wholesaler.name}
</div>
<div className="text-xs text-white/60 mb-1">
ИНН: {wholesaler.inn}
</div>
<div className="text-xs text-white/60 mb-1">
{wholesaler.address}
</div>
<div className="text-xs text-white/60">
{wholesaler.contact}
</div>
</div>
</td>
<td className="p-4">
<span className="text-white/80">
{wholesaler.products.reduce(
(sum, p) => sum + p.plannedQty,
0
)}
</span>
</td>
<td className="p-4">
<span className="text-white/80">
{wholesaler.products.reduce(
(sum, p) => sum + p.actualQty,
0
)}
</span>
</td>
<td className="p-4">
<span className="text-white/80">
{wholesaler.products.reduce(
(sum, p) => sum + p.defectQty,
0
)}
</span>
</td>
<td className="p-4">
<span className="text-green-400 font-medium">
{formatCurrency(
wholesaler.products.reduce(
(sum, p) =>
sum + calculateProductTotal(p),
0
)
)}
</span>
</td>
<td className="p-4" colSpan={2}></td>
<td className="p-4">
<span className="text-white font-semibold">
{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">
<td className="p-4 pl-28">
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
toggleProductExpansion(
product.id
)
}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
{isProductExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
<Package className="h-4 w-4 text-yellow-400" />
<span className="text-white font-medium">
Товар
</span>
</div>
</td>
<td className="p-4" colSpan={2}>
<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>
<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" colSpan={2}>
{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>
{/* Параметры товара */}
{isProductExpanded && (
<tr>
<td
colSpan={11}
className="p-0"
>
<div className="bg-white/5 border-t border-white/10">
<div className="p-4 pl-36">
<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>
);
}