Обновлен компонент панели поставок: изменены вкладки для товаров и расходников, добавлены новые вкладки для детального просмотра товаров и расходников. Обновлены интерфейсы и логика отображения данных, улучшена читаемость кода. Добавлена интеграция с GraphQL для получения данных о сотрудниках и логистических партнерах.
This commit is contained in:
@ -1,54 +1,66 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from "@/components/ui/card";
|
||||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||||
import { useSidebar } from '@/hooks/useSidebar'
|
import { useSidebar } from "@/hooks/useSidebar";
|
||||||
import { Package, Truck, Wrench, ArrowLeftRight } from 'lucide-react'
|
import { Building2, ShoppingCart } from "lucide-react";
|
||||||
|
|
||||||
// Импорты компонентов подразделов
|
// Импорты компонентов подразделов
|
||||||
import { GoodsSuppliesTab } from './goods-supplies/goods-supplies-tab'
|
import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab";
|
||||||
import { MaterialsSuppliesTab } from './materials-supplies/materials-supplies-tab'
|
import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab";
|
||||||
|
|
||||||
export function FulfillmentSuppliesDashboard() {
|
export function FulfillmentSuppliesDashboard() {
|
||||||
const { getSidebarMargin } = useSidebar()
|
const { getSidebarMargin } = useSidebar();
|
||||||
const [activeTab, setActiveTab] = useState('goods')
|
const [activeTab, setActiveTab] = useState("fulfillment");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex overflow-hidden">
|
<div className="h-screen flex overflow-hidden">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 overflow-hidden transition-all duration-300`}>
|
<main
|
||||||
|
className={`flex-1 ${getSidebarMargin()} px-4 py-3 overflow-hidden transition-all duration-300`}
|
||||||
|
>
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
{/* Основной контент с табами */}
|
{/* Основной контент с табами */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="h-full flex flex-col"
|
||||||
|
>
|
||||||
<TabsList className="grid w-full grid-cols-2 bg-white/5 backdrop-blur border-white/10 flex-shrink-0 h-10">
|
<TabsList className="grid w-full grid-cols-2 bg-white/5 backdrop-blur border-white/10 flex-shrink-0 h-10">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="goods"
|
value="fulfillment"
|
||||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-sm"
|
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-sm"
|
||||||
>
|
>
|
||||||
<Package className="h-3 w-3" />
|
<Building2 className="h-3 w-3" />
|
||||||
Товары
|
Поставки на ФФ
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="materials"
|
value="marketplace"
|
||||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-sm"
|
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-sm"
|
||||||
>
|
>
|
||||||
<Wrench className="h-3 w-3" />
|
<ShoppingCart className="h-3 w-3" />
|
||||||
Расходники
|
Поставки на маркетплейсы
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="goods" className="flex-1 overflow-hidden mt-3">
|
<TabsContent
|
||||||
|
value="fulfillment"
|
||||||
|
className="flex-1 overflow-hidden mt-3"
|
||||||
|
>
|
||||||
<Card className="glass-card h-full overflow-hidden p-0">
|
<Card className="glass-card h-full overflow-hidden p-0">
|
||||||
<GoodsSuppliesTab />
|
<FulfillmentSuppliesTab />
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="materials" className="flex-1 overflow-hidden mt-3">
|
<TabsContent
|
||||||
|
value="marketplace"
|
||||||
|
className="flex-1 overflow-hidden mt-3"
|
||||||
|
>
|
||||||
<Card className="glass-card h-full overflow-hidden p-0">
|
<Card className="glass-card h-full overflow-hidden p-0">
|
||||||
<MaterialsSuppliesTab />
|
<MarketplaceSuppliesTab />
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@ -56,5 +68,5 @@ export function FulfillmentSuppliesDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,881 @@
|
|||||||
|
"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 {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Calendar,
|
||||||
|
Package,
|
||||||
|
MapPin,
|
||||||
|
Building2,
|
||||||
|
TrendingUp,
|
||||||
|
AlertTriangle,
|
||||||
|
DollarSign,
|
||||||
|
} 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 Seller {
|
||||||
|
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;
|
||||||
|
sellers: Seller[];
|
||||||
|
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 mockFulfillmentGoodsSupplies: 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,
|
||||||
|
sellers: [
|
||||||
|
{
|
||||||
|
id: "ffs1",
|
||||||
|
name: 'ООО "ТехноСнаб ФФ"',
|
||||||
|
inn: "7701234567",
|
||||||
|
contact: "+7 (495) 123-45-67",
|
||||||
|
address: "Москва, ул. Торговая, 1",
|
||||||
|
totalAmount: 3600000,
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
id: "ffp1",
|
||||||
|
name: "Смартфон iPhone 15 для ФФ",
|
||||||
|
sku: "APL-IP15-128-FF",
|
||||||
|
category: "Электроника ФФ",
|
||||||
|
plannedQty: 50,
|
||||||
|
actualQty: 48,
|
||||||
|
defectQty: 2,
|
||||||
|
productPrice: 75000,
|
||||||
|
parameters: [
|
||||||
|
{ id: "ffparam1", name: "Цвет", value: "Черный" },
|
||||||
|
{ id: "ffparam2", name: "Память", value: "128", unit: "ГБ" },
|
||||||
|
{
|
||||||
|
id: "ffparam3",
|
||||||
|
name: "Гарантия",
|
||||||
|
value: "12",
|
||||||
|
unit: "мес",
|
||||||
|
},
|
||||||
|
{ id: "ffparam4", name: "Упаковка ФФ", value: "Усиленная" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: "WB Подольск ФФ",
|
||||||
|
toAddress: "Подольск, ул. Складская, 25",
|
||||||
|
totalProductPrice: 750000,
|
||||||
|
fulfillmentServicePrice: 18000,
|
||||||
|
logisticsPrice: 12000,
|
||||||
|
totalAmount: 780000,
|
||||||
|
sellers: [
|
||||||
|
{
|
||||||
|
id: "ffs2",
|
||||||
|
name: 'ООО "АудиоТех ФФ"',
|
||||||
|
inn: "7702345678",
|
||||||
|
contact: "+7 (495) 555-12-34",
|
||||||
|
address: "Москва, ул. Звуковая, 8",
|
||||||
|
totalAmount: 750000,
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
id: "ffp2",
|
||||||
|
name: "Наушники AirPods Pro для ФФ",
|
||||||
|
sku: "APL-AP-PRO2-FF",
|
||||||
|
category: "Аудио ФФ",
|
||||||
|
plannedQty: 30,
|
||||||
|
actualQty: 30,
|
||||||
|
defectQty: 0,
|
||||||
|
productPrice: 25000,
|
||||||
|
parameters: [
|
||||||
|
{ id: "ffparam5", name: "Тип", value: "Беспроводные" },
|
||||||
|
{ id: "ffparam6", name: "Шумоподавление", value: "Активное" },
|
||||||
|
{
|
||||||
|
id: "ffparam7",
|
||||||
|
name: "Время работы",
|
||||||
|
value: "6",
|
||||||
|
unit: "ч",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ffparam8",
|
||||||
|
name: "Сертификация ФФ",
|
||||||
|
value: "Пройдена",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FulfillmentDetailedGoodsTab() {
|
||||||
|
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedSellers, setExpandedSellers] = 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 toggleSellerExpansion = (sellerId: string) => {
|
||||||
|
const newExpanded = new Set(expandedSellers);
|
||||||
|
if (newExpanded.has(sellerId)) {
|
||||||
|
newExpanded.delete(sellerId);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(sellerId);
|
||||||
|
}
|
||||||
|
setExpandedSellers(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="space-y-6">
|
||||||
|
{/* Статистика товаров ФФ */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-blue-500/20 rounded-lg">
|
||||||
|
<Package className="h-5 w-5 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">Поставок товаров ФФ</p>
|
||||||
|
<p className="text-xl font-bold text-white">
|
||||||
|
{mockFulfillmentGoodsSupplies.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-green-500/20 rounded-lg">
|
||||||
|
<TrendingUp className="h-5 w-5 text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">Сумма товаров ФФ</p>
|
||||||
|
<p className="text-xl font-bold text-white">
|
||||||
|
{formatCurrency(
|
||||||
|
mockFulfillmentGoodsSupplies.reduce(
|
||||||
|
(sum, supply) => sum + supply.grandTotal,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-yellow-500/20 rounded-lg">
|
||||||
|
<Calendar className="h-5 w-5 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">В пути</p>
|
||||||
|
<p className="text-xl font-bold text-white">
|
||||||
|
{
|
||||||
|
mockFulfillmentGoodsSupplies.filter(
|
||||||
|
(supply) => supply.status === "in-transit"
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-red-500/20 rounded-lg">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">С браком</p>
|
||||||
|
<p className="text-xl font-bold text-white">
|
||||||
|
{
|
||||||
|
mockFulfillmentGoodsSupplies.filter(
|
||||||
|
(supply) => supply.defectTotal > 0
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Таблица поставок товаров ФФ */}
|
||||||
|
<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">
|
||||||
|
Дата поставки
|
||||||
|
</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>
|
||||||
|
{mockFulfillmentGoodsSupplies.map((supply) => {
|
||||||
|
const isSupplyExpanded = expandedSupplies.has(supply.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={supply.id}>
|
||||||
|
{/* Уровень 1: Основная строка поставки ФФ */}
|
||||||
|
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors bg-purple-500/10">
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleSupplyExpansion(supply.id)}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{isSupplyExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<span className="text-white font-bold 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.sellers.reduce(
|
||||||
|
(sum, s) =>
|
||||||
|
sum +
|
||||||
|
s.products.reduce(
|
||||||
|
(pSum, p) => pSum + p.plannedQty,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{route.sellers.reduce(
|
||||||
|
(sum, s) =>
|
||||||
|
sum +
|
||||||
|
s.products.reduce(
|
||||||
|
(pSum, p) => pSum + p.actualQty,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{route.sellers.reduce(
|
||||||
|
(sum, s) =>
|
||||||
|
sum +
|
||||||
|
s.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.sellers.map((seller) => {
|
||||||
|
const isSellerExpanded = expandedSellers.has(
|
||||||
|
seller.id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<React.Fragment key={seller.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={() =>
|
||||||
|
toggleSellerExpansion(seller.id)
|
||||||
|
}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{isSellerExpanded ? (
|
||||||
|
<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">
|
||||||
|
{seller.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60 mb-1">
|
||||||
|
ИНН: {seller.inn}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60 mb-1">
|
||||||
|
{seller.address}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60">
|
||||||
|
{seller.contact}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{seller.products.reduce(
|
||||||
|
(sum, p) => sum + p.plannedQty,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{seller.products.reduce(
|
||||||
|
(sum, p) => sum + p.actualQty,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{seller.products.reduce(
|
||||||
|
(sum, p) => sum + p.defectQty,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-green-400 font-medium">
|
||||||
|
{formatCurrency(
|
||||||
|
seller.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(seller.totalAmount)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* Товары */}
|
||||||
|
{isSellerExpanded &&
|
||||||
|
seller.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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,866 @@
|
|||||||
|
"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 "../../supplies/ui/stats-card";
|
||||||
|
import { StatsGrid } from "../../supplies/ui/stats-grid";
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Calendar,
|
||||||
|
MapPin,
|
||||||
|
Building2,
|
||||||
|
TrendingUp,
|
||||||
|
AlertTriangle,
|
||||||
|
DollarSign,
|
||||||
|
Wrench,
|
||||||
|
Box,
|
||||||
|
Package2,
|
||||||
|
Tags,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
// Типы данных для расходников ФФ детально
|
||||||
|
interface ConsumableParameter {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Consumable {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sku: string;
|
||||||
|
category: string;
|
||||||
|
type: "packaging" | "labels" | "protective" | "tools" | "other";
|
||||||
|
plannedQty: number;
|
||||||
|
actualQty: number;
|
||||||
|
defectQty: number;
|
||||||
|
unitPrice: number;
|
||||||
|
parameters: ConsumableParameter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConsumableSupplier {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
inn: string;
|
||||||
|
contact: string;
|
||||||
|
address: string;
|
||||||
|
consumables: Consumable[];
|
||||||
|
totalAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConsumableRoute {
|
||||||
|
id: string;
|
||||||
|
from: string;
|
||||||
|
fromAddress: string;
|
||||||
|
to: string;
|
||||||
|
toAddress: string;
|
||||||
|
suppliers: ConsumableSupplier[];
|
||||||
|
totalConsumablesPrice: number;
|
||||||
|
logisticsPrice: number;
|
||||||
|
totalAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FulfillmentConsumableSupply {
|
||||||
|
id: string;
|
||||||
|
number: number;
|
||||||
|
deliveryDate: string;
|
||||||
|
createdDate: string;
|
||||||
|
routes: ConsumableRoute[];
|
||||||
|
plannedTotal: number;
|
||||||
|
actualTotal: number;
|
||||||
|
defectTotal: number;
|
||||||
|
totalConsumablesPrice: number;
|
||||||
|
totalLogisticsPrice: number;
|
||||||
|
grandTotal: number;
|
||||||
|
status: "planned" | "in-transit" | "delivered" | "completed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Моковые данные для расходников ФФ детально
|
||||||
|
const mockFulfillmentConsumablesDetailed: FulfillmentConsumableSupply[] = [
|
||||||
|
{
|
||||||
|
id: "ffcd1",
|
||||||
|
number: 2001,
|
||||||
|
deliveryDate: "2024-01-18",
|
||||||
|
createdDate: "2024-01-14",
|
||||||
|
status: "delivered",
|
||||||
|
plannedTotal: 5000,
|
||||||
|
actualTotal: 4950,
|
||||||
|
defectTotal: 50,
|
||||||
|
totalConsumablesPrice: 125000,
|
||||||
|
totalLogisticsPrice: 8000,
|
||||||
|
grandTotal: 133000,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: "ffcdr1",
|
||||||
|
from: "Склад расходников ФФ",
|
||||||
|
fromAddress: "Москва, ул. Промышленная, 12",
|
||||||
|
to: "SFERAV Logistics ФФ",
|
||||||
|
toAddress: "Москва, ул. Складская, 15",
|
||||||
|
totalConsumablesPrice: 125000,
|
||||||
|
logisticsPrice: 8000,
|
||||||
|
totalAmount: 133000,
|
||||||
|
suppliers: [
|
||||||
|
{
|
||||||
|
id: "ffcds1",
|
||||||
|
name: 'ООО "УпакСервис ФФ Детально"',
|
||||||
|
inn: "7703456789",
|
||||||
|
contact: "+7 (495) 777-88-99",
|
||||||
|
address: "Москва, ул. Упаковочная, 5",
|
||||||
|
totalAmount: 75000,
|
||||||
|
consumables: [
|
||||||
|
{
|
||||||
|
id: "ffcdcons1",
|
||||||
|
name: "Коробки для ФФ детально 40x30x15",
|
||||||
|
sku: "BOX-FFD-403015",
|
||||||
|
category: "Упаковка ФФ детально",
|
||||||
|
type: "packaging",
|
||||||
|
plannedQty: 2000,
|
||||||
|
actualQty: 1980,
|
||||||
|
defectQty: 20,
|
||||||
|
unitPrice: 45,
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
id: "ffcdp1",
|
||||||
|
name: "Размер",
|
||||||
|
value: "40x30x15",
|
||||||
|
unit: "см",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ffcdp2",
|
||||||
|
name: "Материал",
|
||||||
|
value: "Гофрокартон усиленный ФФ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ffcdp3",
|
||||||
|
name: "Плотность",
|
||||||
|
value: "5",
|
||||||
|
unit: "слоев",
|
||||||
|
},
|
||||||
|
{ id: "ffcdp4", name: "Сертификация ФФ", value: "Пройдена" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ffcd2",
|
||||||
|
number: 2002,
|
||||||
|
deliveryDate: "2024-01-22",
|
||||||
|
createdDate: "2024-01-16",
|
||||||
|
status: "in-transit",
|
||||||
|
plannedTotal: 3000,
|
||||||
|
actualTotal: 3000,
|
||||||
|
defectTotal: 0,
|
||||||
|
totalConsumablesPrice: 85000,
|
||||||
|
totalLogisticsPrice: 5500,
|
||||||
|
grandTotal: 90500,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: "ffcdr2",
|
||||||
|
from: "Склад расходников ФФ",
|
||||||
|
fromAddress: "Москва, ул. Промышленная, 12",
|
||||||
|
to: "WB Подольск ФФ",
|
||||||
|
toAddress: "Подольск, ул. Складская, 25",
|
||||||
|
totalConsumablesPrice: 85000,
|
||||||
|
logisticsPrice: 5500,
|
||||||
|
totalAmount: 90500,
|
||||||
|
suppliers: [
|
||||||
|
{
|
||||||
|
id: "ffcds2",
|
||||||
|
name: 'ООО "ЭтикеткаПро ФФ"',
|
||||||
|
inn: "7704567890",
|
||||||
|
contact: "+7 (495) 888-99-00",
|
||||||
|
address: "Москва, ул. Этикеточная, 3",
|
||||||
|
totalAmount: 85000,
|
||||||
|
consumables: [
|
||||||
|
{
|
||||||
|
id: "ffcdcons2",
|
||||||
|
name: "Этикетки самоклеящиеся ФФ 10x5",
|
||||||
|
sku: "LBL-FFD-105",
|
||||||
|
category: "Этикетки ФФ детально",
|
||||||
|
type: "labels",
|
||||||
|
plannedQty: 3000,
|
||||||
|
actualQty: 3000,
|
||||||
|
defectQty: 0,
|
||||||
|
unitPrice: 28,
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
id: "ffcdp5",
|
||||||
|
name: "Размер",
|
||||||
|
value: "10x5",
|
||||||
|
unit: "см",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ffcdp6",
|
||||||
|
name: "Материал",
|
||||||
|
value: "Бумага самоклеящаяся ФФ",
|
||||||
|
},
|
||||||
|
{ id: "ffcdp7", name: "Клей", value: "Акриловый" },
|
||||||
|
{ id: "ffcdp8", name: "Качество ФФ", value: "Премиум" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FulfillmentDetailedSuppliesTab() {
|
||||||
|
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedSuppliers, setExpandedSuppliers] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [expandedConsumables, setExpandedConsumables] = 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 toggleSupplierExpansion = (supplierId: string) => {
|
||||||
|
const newExpanded = new Set(expandedSuppliers);
|
||||||
|
if (newExpanded.has(supplierId)) {
|
||||||
|
newExpanded.delete(supplierId);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(supplierId);
|
||||||
|
}
|
||||||
|
setExpandedSuppliers(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleConsumableExpansion = (consumableId: string) => {
|
||||||
|
const newExpanded = new Set(expandedConsumables);
|
||||||
|
if (newExpanded.has(consumableId)) {
|
||||||
|
newExpanded.delete(consumableId);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(consumableId);
|
||||||
|
}
|
||||||
|
setExpandedConsumables(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: FulfillmentConsumableSupply["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 getTypeBadge = (type: Consumable["type"]) => {
|
||||||
|
const typeMap = {
|
||||||
|
packaging: {
|
||||||
|
label: "Упаковка",
|
||||||
|
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
label: "Этикетки",
|
||||||
|
color: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||||
|
},
|
||||||
|
protective: {
|
||||||
|
label: "Защитная",
|
||||||
|
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
label: "Инструменты",
|
||||||
|
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
label: "Прочее",
|
||||||
|
color: "bg-gray-500/20 text-gray-300 border-gray-500/30",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { label, color } = typeMap[type];
|
||||||
|
return <Badge className={`${color} border text-xs`}>{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 calculateConsumableTotal = (consumable: Consumable) => {
|
||||||
|
return consumable.actualQty * consumable.unitPrice;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Статистика расходников ФФ детально */}
|
||||||
|
<StatsGrid>
|
||||||
|
<StatsCard
|
||||||
|
title="Расходники ФФ детально"
|
||||||
|
value={mockFulfillmentConsumablesDetailed.length}
|
||||||
|
icon={Package2}
|
||||||
|
iconColor="text-orange-400"
|
||||||
|
iconBg="bg-orange-500/20"
|
||||||
|
trend={{ value: 5, isPositive: true }}
|
||||||
|
subtitle="Поставки материалов ФФ"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="Сумма расходников ФФ детально"
|
||||||
|
value={formatCurrency(
|
||||||
|
mockFulfillmentConsumablesDetailed.reduce(
|
||||||
|
(sum, supply) => sum + supply.grandTotal,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
icon={TrendingUp}
|
||||||
|
iconColor="text-green-400"
|
||||||
|
iconBg="bg-green-500/20"
|
||||||
|
trend={{ value: 15, isPositive: true }}
|
||||||
|
subtitle="Общая стоимость ФФ"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="В пути"
|
||||||
|
value={
|
||||||
|
mockFulfillmentConsumablesDetailed.filter(
|
||||||
|
(supply) => supply.status === "in-transit"
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
icon={Calendar}
|
||||||
|
iconColor="text-yellow-400"
|
||||||
|
iconBg="bg-yellow-500/20"
|
||||||
|
subtitle="Активные поставки ФФ"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCard
|
||||||
|
title="С браком"
|
||||||
|
value={
|
||||||
|
mockFulfillmentConsumablesDetailed.filter(
|
||||||
|
(supply) => supply.defectTotal > 0
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
icon={AlertTriangle}
|
||||||
|
iconColor="text-red-400"
|
||||||
|
iconBg="bg-red-500/20"
|
||||||
|
trend={{ value: 2, isPositive: false }}
|
||||||
|
subtitle="Дефектные материалы ФФ"
|
||||||
|
/>
|
||||||
|
</StatsGrid>
|
||||||
|
|
||||||
|
{/* Таблица поставок расходников ФФ детально */}
|
||||||
|
<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">
|
||||||
|
Дата поставки
|
||||||
|
</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>
|
||||||
|
{mockFulfillmentConsumablesDetailed.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-orange-500/10">
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleSupplyExpansion(supply.id)}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{isSupplyExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<span className="text-white font-bold 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.totalConsumablesPrice)}
|
||||||
|
</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.suppliers.reduce(
|
||||||
|
(sum, s) =>
|
||||||
|
sum +
|
||||||
|
s.consumables.reduce(
|
||||||
|
(cSum, c) => cSum + c.plannedQty,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{route.suppliers.reduce(
|
||||||
|
(sum, s) =>
|
||||||
|
sum +
|
||||||
|
s.consumables.reduce(
|
||||||
|
(cSum, c) => cSum + c.actualQty,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{route.suppliers.reduce(
|
||||||
|
(sum, s) =>
|
||||||
|
sum +
|
||||||
|
s.consumables.reduce(
|
||||||
|
(cSum, c) => cSum + c.defectQty,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-green-400 font-medium">
|
||||||
|
{formatCurrency(route.totalConsumablesPrice)}
|
||||||
|
</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.suppliers.map((supplier) => {
|
||||||
|
const isSupplierExpanded =
|
||||||
|
expandedSuppliers.has(supplier.id);
|
||||||
|
return (
|
||||||
|
<React.Fragment key={supplier.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={() =>
|
||||||
|
toggleSupplierExpansion(
|
||||||
|
supplier.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{isSupplierExpanded ? (
|
||||||
|
<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">
|
||||||
|
{supplier.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60 mb-1">
|
||||||
|
ИНН: {supplier.inn}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60 mb-1">
|
||||||
|
{supplier.address}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60">
|
||||||
|
{supplier.contact}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{supplier.consumables.reduce(
|
||||||
|
(sum, c) => sum + c.plannedQty,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{supplier.consumables.reduce(
|
||||||
|
(sum, c) => sum + c.actualQty,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{supplier.consumables.reduce(
|
||||||
|
(sum, c) => sum + c.defectQty,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-green-400 font-medium">
|
||||||
|
{formatCurrency(
|
||||||
|
supplier.consumables.reduce(
|
||||||
|
(sum, c) =>
|
||||||
|
sum +
|
||||||
|
calculateConsumableTotal(c),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4" colSpan={1}></td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white font-semibold">
|
||||||
|
{formatCurrency(supplier.totalAmount)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* Расходники */}
|
||||||
|
{isSupplierExpanded &&
|
||||||
|
supplier.consumables.map((consumable) => {
|
||||||
|
const isConsumableExpanded =
|
||||||
|
expandedConsumables.has(
|
||||||
|
consumable.id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<React.Fragment key={consumable.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={() =>
|
||||||
|
toggleConsumableExpansion(
|
||||||
|
consumable.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{isConsumableExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Wrench 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">
|
||||||
|
{consumable.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60 mb-1">
|
||||||
|
Артикул: {consumable.sku}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs">
|
||||||
|
{consumable.category}
|
||||||
|
</Badge>
|
||||||
|
{getTypeBadge(
|
||||||
|
consumable.type
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white font-semibold">
|
||||||
|
{consumable.plannedQty}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white font-semibold">
|
||||||
|
{consumable.actualQty}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span
|
||||||
|
className={`font-semibold ${
|
||||||
|
consumable.defectQty > 0
|
||||||
|
? "text-red-400"
|
||||||
|
: "text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{consumable.defectQty}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="text-white">
|
||||||
|
<div className="font-medium">
|
||||||
|
{formatCurrency(
|
||||||
|
calculateConsumableTotal(
|
||||||
|
consumable
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-white/60">
|
||||||
|
{formatCurrency(
|
||||||
|
consumable.unitPrice
|
||||||
|
)}{" "}
|
||||||
|
за шт.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="p-4"
|
||||||
|
colSpan={1}
|
||||||
|
></td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white font-semibold">
|
||||||
|
{formatCurrency(
|
||||||
|
calculateConsumableTotal(
|
||||||
|
consumable
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* Параметры расходника ФФ */}
|
||||||
|
{isConsumableExpanded && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={10}
|
||||||
|
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">
|
||||||
|
{consumable.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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,61 +1,149 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { GET_MY_EMPLOYEES, GET_MY_COUNTERPARTIES } from "@/graphql/queries";
|
||||||
import {
|
import {
|
||||||
Package,
|
Package,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
AlertCircle,
|
|
||||||
Calendar,
|
Calendar,
|
||||||
Eye,
|
Eye,
|
||||||
|
User,
|
||||||
|
Truck,
|
||||||
|
Hash,
|
||||||
|
Package2,
|
||||||
|
Boxes,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Store,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
// Мок данные для товаров
|
// Интерфейсы для данных
|
||||||
const mockGoodsSupplies = [
|
interface Employee {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
position: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Organization {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
fullName?: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Мок данные для поставок с новой структурой
|
||||||
|
const mockFulfillmentSupplies = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
productName: "Смартфон iPhone 15",
|
supplyNumber: "ФФ-2024-001",
|
||||||
sku: "IPH15-128-BLK",
|
supplyDate: "2024-01-15",
|
||||||
seller: "TechStore LLC",
|
seller: {
|
||||||
quantity: 50,
|
id: "seller1",
|
||||||
expectedDate: "2024-01-15",
|
name: "TechStore LLC",
|
||||||
|
storeName: "ТехноМагазин",
|
||||||
|
managerName: "Иванов Иван Иванович",
|
||||||
|
phone: "+7 (495) 123-45-67",
|
||||||
|
email: "contact@techstore.ru",
|
||||||
|
inn: "7701234567",
|
||||||
|
},
|
||||||
|
itemsQuantity: 150,
|
||||||
|
cargoPlaces: 5,
|
||||||
|
volume: 12.5,
|
||||||
|
responsibleEmployeeId: "emp1",
|
||||||
|
logisticsPartnerId: "log1",
|
||||||
status: "planned",
|
status: "planned",
|
||||||
totalValue: 2500000,
|
totalValue: 2500000,
|
||||||
warehouse: "Склад А1",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
productName: "Ноутбук MacBook Air",
|
supplyNumber: "ФФ-2024-002",
|
||||||
sku: "MBA-M2-256-SLV",
|
supplyDate: "2024-01-12",
|
||||||
seller: "Apple Reseller",
|
seller: {
|
||||||
quantity: 25,
|
id: "seller2",
|
||||||
expectedDate: "2024-01-12",
|
name: "Apple Reseller",
|
||||||
|
storeName: "ЭплСтор",
|
||||||
|
managerName: "Петров Петр Петрович",
|
||||||
|
phone: "+7 (495) 987-65-43",
|
||||||
|
email: "orders@applereseller.ru",
|
||||||
|
inn: "7709876543",
|
||||||
|
},
|
||||||
|
itemsQuantity: 75,
|
||||||
|
cargoPlaces: 3,
|
||||||
|
volume: 8.2,
|
||||||
|
responsibleEmployeeId: "emp2",
|
||||||
|
logisticsPartnerId: "log2",
|
||||||
status: "in-transit",
|
status: "in-transit",
|
||||||
totalValue: 3750000,
|
totalValue: 3750000,
|
||||||
warehouse: "Склад Б2",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
productName: "Наушники AirPods Pro",
|
supplyNumber: "ФФ-2024-003",
|
||||||
sku: "APP-2GEN-WHT",
|
supplyDate: "2024-01-10",
|
||||||
seller: "Audio World",
|
seller: {
|
||||||
quantity: 100,
|
id: "seller3",
|
||||||
expectedDate: "2024-01-10",
|
name: "Audio World",
|
||||||
|
storeName: "АудиоМир",
|
||||||
|
managerName: "Сидоров Сидор Сидорович",
|
||||||
|
phone: "+7 (495) 555-12-34",
|
||||||
|
email: "info@audioworld.ru",
|
||||||
|
inn: "7705551234",
|
||||||
|
},
|
||||||
|
itemsQuantity: 200,
|
||||||
|
cargoPlaces: 8,
|
||||||
|
volume: 15.7,
|
||||||
|
responsibleEmployeeId: "emp1",
|
||||||
|
logisticsPartnerId: "log1",
|
||||||
status: "delivered",
|
status: "delivered",
|
||||||
totalValue: 2800000,
|
totalValue: 2800000,
|
||||||
warehouse: "Склад А1",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function FulfillmentGoodsTab() {
|
export function FulfillmentGoodsTab() {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
|
const [expandedSellers, setExpandedSellers] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Загружаем сотрудников для селектора ответственных
|
||||||
|
const { data: employeesData, loading: employeesLoading } =
|
||||||
|
useQuery(GET_MY_EMPLOYEES);
|
||||||
|
|
||||||
|
// Загружаем партнеров-логистов
|
||||||
|
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(
|
||||||
|
GET_MY_COUNTERPARTIES
|
||||||
|
);
|
||||||
|
|
||||||
|
const employees: Employee[] = employeesData?.myEmployees || [];
|
||||||
|
const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter(
|
||||||
|
(org: Organization) => org.type === "LOGIST"
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleSellerExpansion = (sellerId: string) => {
|
||||||
|
const newExpanded = new Set(expandedSellers);
|
||||||
|
if (newExpanded.has(sellerId)) {
|
||||||
|
newExpanded.delete(sellerId);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(sellerId);
|
||||||
|
}
|
||||||
|
setExpandedSellers(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount: number) => {
|
||||||
return new Intl.NumberFormat("ru-RU", {
|
return new Intl.NumberFormat("ru-RU", {
|
||||||
@ -103,11 +191,30 @@ export function FulfillmentGoodsTab() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredSupplies = mockGoodsSupplies.filter((supply) => {
|
const getEmployeeName = (employeeId: string) => {
|
||||||
|
const employee = employees.find((emp) => emp.id === employeeId);
|
||||||
|
return employee
|
||||||
|
? `${employee.firstName} ${employee.lastName}`
|
||||||
|
: "Не назначен";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogisticsPartnerName = (partnerId: string) => {
|
||||||
|
const partner = logisticsPartners.find(
|
||||||
|
(p: Organization) => p.id === partnerId
|
||||||
|
);
|
||||||
|
return partner
|
||||||
|
? partner.name || partner.fullName || "Без названия"
|
||||||
|
: "Не выбран";
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredSupplies = mockFulfillmentSupplies.filter((supply) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
supply.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
supply.supplyNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
supply.sku.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
supply.seller.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
supply.seller.toLowerCase().includes(searchTerm.toLowerCase());
|
supply.seller.storeName
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase()) ||
|
||||||
|
supply.seller.inn.includes(searchTerm);
|
||||||
|
|
||||||
const matchesStatus =
|
const matchesStatus =
|
||||||
statusFilter === "all" || supply.status === statusFilter;
|
statusFilter === "all" || supply.status === statusFilter;
|
||||||
@ -120,14 +227,21 @@ export function FulfillmentGoodsTab() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTotalQuantity = () => {
|
const getTotalQuantity = () => {
|
||||||
return filteredSupplies.reduce((sum, supply) => sum + supply.quantity, 0);
|
return filteredSupplies.reduce(
|
||||||
|
(sum, supply) => sum + supply.itemsQuantity,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalVolume = () => {
|
||||||
|
return filteredSupplies.reduce((sum, supply) => sum + supply.volume, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col space-y-4 p-4">
|
<div className="h-full flex flex-col space-y-4 p-4">
|
||||||
{/* Статистика с кнопкой */}
|
{/* Статистика с кнопкой */}
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3 flex-1">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 flex-1">
|
||||||
<Card className="glass-card p-3 h-[60px]">
|
<Card className="glass-card p-3 h-[60px]">
|
||||||
<div className="flex items-center space-x-2 h-full">
|
<div className="flex items-center space-x-2 h-full">
|
||||||
<div className="p-1.5 bg-blue-500/20 rounded">
|
<div className="p-1.5 bg-blue-500/20 rounded">
|
||||||
@ -159,12 +273,26 @@ export function FulfillmentGoodsTab() {
|
|||||||
<Card className="glass-card p-3 h-[60px]">
|
<Card className="glass-card p-3 h-[60px]">
|
||||||
<div className="flex items-center space-x-2 h-full">
|
<div className="flex items-center space-x-2 h-full">
|
||||||
<div className="p-1.5 bg-purple-500/20 rounded">
|
<div className="p-1.5 bg-purple-500/20 rounded">
|
||||||
<AlertCircle className="h-3 w-3 text-purple-400" />
|
<Package2 className="h-3 w-3 text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white/60 text-xs">Товаров</p>
|
<p className="text-white/60 text-xs">Товаров</p>
|
||||||
<p className="text-lg font-bold text-white">
|
<p className="text-lg font-bold text-white">
|
||||||
{getTotalQuantity()}
|
{getTotalQuantity()} ед.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="glass-card p-3 h-[60px]">
|
||||||
|
<div className="flex items-center space-x-2 h-full">
|
||||||
|
<div className="p-1.5 bg-orange-500/20 rounded">
|
||||||
|
<Boxes className="h-3 w-3 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">Объём</p>
|
||||||
|
<p className="text-lg font-bold text-white">
|
||||||
|
{getTotalVolume().toFixed(1)} м³
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -185,7 +313,7 @@ export function FulfillmentGoodsTab() {
|
|||||||
<div className="relative flex-1 max-w-md">
|
<div className="relative flex-1 max-w-md">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Поиск по товарам, SKU, селлерам..."
|
placeholder="Поиск по номеру, магазину, ИНН..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="glass-input pl-10 text-white placeholder:text-white/40"
|
className="glass-input pl-10 text-white placeholder:text-white/40"
|
||||||
@ -208,71 +336,240 @@ export function FulfillmentGoodsTab() {
|
|||||||
{/* Список поставок */}
|
{/* Список поставок */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<div className="h-full overflow-y-auto space-y-3">
|
<div className="h-full overflow-y-auto space-y-3">
|
||||||
{filteredSupplies.map((supply) => (
|
{filteredSupplies.map((supply) => {
|
||||||
<Card
|
const isSellerExpanded = expandedSellers.has(supply.seller.id);
|
||||||
key={supply.id}
|
|
||||||
className="glass-card p-4 hover:bg-white/10 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<h3 className="text-white font-medium">
|
|
||||||
{supply.productName}
|
|
||||||
</h3>
|
|
||||||
{getStatusBadge(supply.status)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
return (
|
||||||
<div>
|
<Card
|
||||||
<p className="text-white/60">SKU</p>
|
key={supply.id}
|
||||||
<p className="text-white font-mono">{supply.sku}</p>
|
className="glass-card p-4 hover:bg-white/10 transition-colors"
|
||||||
</div>
|
>
|
||||||
<div>
|
<div className="space-y-3">
|
||||||
<p className="text-white/60">Селлер</p>
|
{/* Компактный блок с названием магазина */}
|
||||||
<p className="text-white">{supply.seller}</p>
|
<div className="flex items-center justify-between bg-white/5 rounded-lg p-2">
|
||||||
</div>
|
<div className="flex items-center gap-3">
|
||||||
<div>
|
<div className="p-1.5 bg-green-500/20 rounded">
|
||||||
<p className="text-white/60">Количество</p>
|
<Store className="h-3 w-3 text-green-400" />
|
||||||
<p className="text-white font-semibold">
|
</div>
|
||||||
{supply.quantity} шт.
|
<div className="flex items-center gap-2">
|
||||||
</p>
|
<span className="text-white font-medium text-sm">
|
||||||
</div>
|
{supply.seller.storeName}
|
||||||
<div>
|
|
||||||
<p className="text-white/60">Ожидается</p>
|
|
||||||
<p className="text-white">
|
|
||||||
{formatDate(supply.expectedDate)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4 text-sm">
|
|
||||||
<span className="text-white/60">
|
|
||||||
Склад:{" "}
|
|
||||||
<span className="text-white">{supply.warehouse}</span>
|
|
||||||
</span>
|
|
||||||
<span className="text-white/60">
|
|
||||||
Стоимость:{" "}
|
|
||||||
<span className="text-green-400 font-semibold">
|
|
||||||
{formatCurrency(supply.totalValue)}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
<span className="text-white/60 text-xs"> • </span>
|
||||||
|
<span className="text-white/80 text-xs">
|
||||||
|
{supply.seller.managerName}
|
||||||
|
</span>
|
||||||
|
<span className="text-white/60 text-xs"> • </span>
|
||||||
|
<span className="text-white/80 text-xs">
|
||||||
|
{supply.seller.phone}
|
||||||
|
</span>
|
||||||
|
<span className="text-white/60 text-xs"> • </span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<a
|
||||||
|
href={`https://t.me/${supply.seller.phone.replace(
|
||||||
|
/[^\d]/g,
|
||||||
|
""
|
||||||
|
)}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="p-1 bg-blue-500/20 rounded hover:bg-blue-500/30 transition-colors"
|
||||||
|
title="Написать в Telegram"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-3 w-3 text-blue-400"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`https://wa.me/${supply.seller.phone.replace(
|
||||||
|
/[^\d]/g,
|
||||||
|
""
|
||||||
|
)}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="p-1 bg-green-500/20 rounded hover:bg-green-500/30 transition-colors"
|
||||||
|
title="Написать в WhatsApp"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-3 w-3 text-green-400"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.885 3.488" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleSellerExpansion(supply.seller.id)}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{isSellerExpanded ? (
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Скрытая детальная информация о магазине */}
|
||||||
|
{isSellerExpanded && (
|
||||||
|
<div className="bg-white/5 rounded-lg p-3 space-y-2">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">
|
||||||
|
Юридическое название
|
||||||
|
</p>
|
||||||
|
<p className="text-white text-sm">
|
||||||
|
{supply.seller.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">ИНН</p>
|
||||||
|
<p className="text-white text-sm font-mono">
|
||||||
|
{supply.seller.inn}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white/60 text-xs">Email</p>
|
||||||
|
<p className="text-white text-sm">
|
||||||
|
{supply.seller.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Единый блок со всеми параметрами в одной строке */}
|
||||||
|
<div className="bg-white/5 rounded-lg p-4">
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-9 gap-4 items-center">
|
||||||
|
{/* Номер поставки */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Номер</p>
|
||||||
|
<p className="text-white font-semibold text-sm">
|
||||||
|
{supply.supplyNumber}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Дата поставки */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Дата</p>
|
||||||
|
<p className="text-white font-semibold text-sm">
|
||||||
|
{formatDate(supply.supplyDate)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Количество товаров */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Товаров</p>
|
||||||
|
<p className="text-white font-semibold text-sm">
|
||||||
|
{supply.itemsQuantity}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Количество мест */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Мест</p>
|
||||||
|
<p className="text-white font-semibold text-sm">
|
||||||
|
{supply.cargoPlaces}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Объём */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Объём</p>
|
||||||
|
<p className="text-white font-semibold text-sm">
|
||||||
|
{supply.volume} м³
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Стоимость */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Стоимость</p>
|
||||||
|
<p className="text-green-400 font-semibold text-sm">
|
||||||
|
{formatCurrency(supply.totalValue)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ответственный сотрудник */}
|
||||||
|
<div className="col-span-1">
|
||||||
|
<p className="text-white/60 text-xs mb-1">
|
||||||
|
Ответственный
|
||||||
|
</p>
|
||||||
|
<Select defaultValue={supply.responsibleEmployeeId}>
|
||||||
|
<SelectTrigger className="h-8 glass-input bg-white/10 border-white/20 text-white hover:bg-white/15 focus:bg-white/15 focus:ring-1 focus:ring-purple-400/50 text-xs">
|
||||||
|
<SelectValue placeholder="Выберите" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white">
|
||||||
|
{employees.map((employee) => (
|
||||||
|
<SelectItem
|
||||||
|
key={employee.id}
|
||||||
|
value={employee.id}
|
||||||
|
className="text-white hover:bg-white/10 focus:bg-white/10 text-xs"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">
|
||||||
|
{employee.firstName} {employee.lastName}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-white/60">
|
||||||
|
{employee.position}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Логистический партнер */}
|
||||||
|
<div className="col-span-1">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Логистика</p>
|
||||||
|
<Select defaultValue={supply.logisticsPartnerId}>
|
||||||
|
<SelectTrigger className="h-8 glass-input bg-white/10 border-white/20 text-white hover:bg-white/15 focus:bg-white/15 focus:ring-1 focus:ring-orange-400/50 text-xs">
|
||||||
|
<SelectValue placeholder="Выберите" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white">
|
||||||
|
{logisticsPartners.map((partner: Organization) => (
|
||||||
|
<SelectItem
|
||||||
|
key={partner.id}
|
||||||
|
value={partner.id}
|
||||||
|
className="text-white hover:bg-white/10 focus:bg-white/10 text-xs"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">
|
||||||
|
{partner.name ||
|
||||||
|
partner.fullName ||
|
||||||
|
"Без названия"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-white/60">
|
||||||
|
Логистический партнер
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Статус */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-white/60 text-xs mb-1">Статус</p>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
{getStatusBadge(supply.status)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
<div className="flex items-center gap-2 ml-4">
|
);
|
||||||
<Button
|
})}
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Package, Wrench, RotateCcw } from "lucide-react";
|
import { Package, Wrench, RotateCcw, Package2, Building2 } from "lucide-react";
|
||||||
|
|
||||||
// Импорты компонентов подкатегорий
|
// Импорты компонентов подкатегорий
|
||||||
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
|
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
|
||||||
import { SellerMaterialsTab } from "./seller-materials-tab";
|
|
||||||
import { PvzReturnsTab } from "./pvz-returns-tab";
|
import { PvzReturnsTab } from "./pvz-returns-tab";
|
||||||
|
import { SuppliesConsumablesTab } from "../../supplies/consumables-supplies/consumables-supplies-tab";
|
||||||
|
|
||||||
|
// Новые компоненты для детального просмотра (копия из supplies модуля)
|
||||||
|
import { FulfillmentDetailedGoodsTab } from "./fulfillment-detailed-goods-tab";
|
||||||
|
import { FulfillmentDetailedSuppliesTab } from "./fulfillment-detailed-supplies-tab";
|
||||||
|
|
||||||
export function FulfillmentSuppliesTab() {
|
export function FulfillmentSuppliesTab() {
|
||||||
const [activeTab, setActiveTab] = useState("goods");
|
const [activeTab, setActiveTab] = useState("goods");
|
||||||
@ -19,20 +23,34 @@ export function FulfillmentSuppliesTab() {
|
|||||||
onValueChange={setActiveTab}
|
onValueChange={setActiveTab}
|
||||||
className="h-full flex flex-col"
|
className="h-full flex flex-col"
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-3 bg-white/10 backdrop-blur border-white/10 flex-shrink-0 h-10 mb-3 mx-4 mt-4">
|
<TabsList className="grid w-full grid-cols-5 bg-white/10 backdrop-blur border-white/10 flex-shrink-0 h-10 mb-3 mx-4 mt-4">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="goods"
|
value="goods"
|
||||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs"
|
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs"
|
||||||
>
|
>
|
||||||
<Package className="h-3 w-3" />
|
<Package className="h-3 w-3" />
|
||||||
Товары
|
Поставки
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="materials"
|
value="detailed-goods"
|
||||||
|
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<Package2 className="h-3 w-3" />
|
||||||
|
Товары детально
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="detailed-supplies"
|
||||||
|
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
Расходники детально
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="consumables"
|
||||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs"
|
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs"
|
||||||
>
|
>
|
||||||
<Wrench className="h-3 w-3" />
|
<Wrench className="h-3 w-3" />
|
||||||
Расходники селлеров
|
Расходники С
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="returns"
|
value="returns"
|
||||||
@ -47,8 +65,25 @@ export function FulfillmentSuppliesTab() {
|
|||||||
<FulfillmentGoodsTab />
|
<FulfillmentGoodsTab />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="materials" className="flex-1 overflow-hidden">
|
<TabsContent value="detailed-goods" className="flex-1 overflow-hidden">
|
||||||
<SellerMaterialsTab />
|
<div className="h-full p-4 overflow-y-auto">
|
||||||
|
<FulfillmentDetailedGoodsTab />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent
|
||||||
|
value="detailed-supplies"
|
||||||
|
className="flex-1 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="h-full p-4 overflow-y-auto">
|
||||||
|
<FulfillmentDetailedSuppliesTab />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="consumables" className="flex-1 overflow-hidden">
|
||||||
|
<div className="h-full p-4 overflow-y-auto">
|
||||||
|
<SuppliesConsumablesTab />
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="returns" className="flex-1 overflow-hidden">
|
<TabsContent value="returns" className="flex-1 overflow-hidden">
|
||||||
|
Reference in New Issue
Block a user