Compare commits
3 Commits
d964b9b6d4
...
0e584749f3
Author | SHA1 | Date | |
---|---|---|---|
0e584749f3 | |||
753ec7b2ec | |||
ead17fd56c |
@ -1,10 +1,10 @@
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { FulfillmentSuppliesDashboard } from "@/components/fulfillment-supplies/fulfillment-supplies-dashboard"
|
||||
import { AuthGuard } from "@/components/auth-guard";
|
||||
import { FulfillmentSuppliesDashboard } from "@/components/fulfillment-supplies/fulfillment-supplies-dashboard";
|
||||
|
||||
export default function FulfillmentSuppliesPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<FulfillmentSuppliesDashboard />
|
||||
</AuthGuard>
|
||||
)
|
||||
);
|
||||
}
|
10
src/app/logistics/page.tsx
Normal file
10
src/app/logistics/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { AuthGuard } from "@/components/auth-guard";
|
||||
import { LogisticsDashboard } from "@/components/logistics/logistics-dashboard";
|
||||
|
||||
export default function LogisticsPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<LogisticsDashboard />
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
@ -83,10 +83,21 @@ export function Sidebar() {
|
||||
}
|
||||
|
||||
const handleSuppliesClick = () => {
|
||||
// Для фулфилмент кабинетов используем новый роут
|
||||
if (user?.organization?.type === 'FULFILLMENT') {
|
||||
// Для каждого типа кабинета свой роут
|
||||
switch (user?.organization?.type) {
|
||||
case 'FULFILLMENT':
|
||||
router.push('/fulfillment-supplies')
|
||||
} else {
|
||||
break
|
||||
case 'SELLER':
|
||||
router.push('/supplies')
|
||||
break
|
||||
case 'WHOLESALE':
|
||||
router.push('/supplies')
|
||||
break
|
||||
case 'LOGIST':
|
||||
router.push('/logistics')
|
||||
break
|
||||
default:
|
||||
router.push('/supplies')
|
||||
}
|
||||
}
|
||||
@ -103,7 +114,7 @@ export function Sidebar() {
|
||||
const isServicesActive = pathname.startsWith('/services')
|
||||
const isWarehouseActive = pathname.startsWith('/warehouse')
|
||||
const isEmployeesActive = pathname.startsWith('/employees')
|
||||
const isSuppliesActive = pathname.startsWith('/supplies') || pathname.startsWith('/fulfillment-supplies')
|
||||
const isSuppliesActive = pathname.startsWith('/supplies') || pathname.startsWith('/fulfillment-supplies') || pathname.startsWith('/logistics')
|
||||
const isPartnersActive = pathname.startsWith('/partners')
|
||||
|
||||
return (
|
||||
@ -270,8 +281,8 @@ export function Sidebar() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Поставки - для селлеров и фулфилмент */}
|
||||
{(user?.organization?.type === 'SELLER' || user?.organization?.type === 'FULFILLMENT') && (
|
||||
{/* Мои поставки - для селлеров */}
|
||||
{user?.organization?.type === 'SELLER' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'} text-left transition-all duration-200 text-xs ${
|
||||
@ -280,10 +291,61 @@ export function Sidebar() {
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Поставки" : ""}
|
||||
title={isCollapsed ? "Мои поставки" : ""}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Поставки</span>}
|
||||
{!isCollapsed && <span className="ml-3">Мои поставки</span>}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Входящие поставки - для фулфилмент */}
|
||||
{user?.organization?.type === 'FULFILLMENT' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'} text-left transition-all duration-200 text-xs ${
|
||||
isSuppliesActive
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Входящие поставки" : ""}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Входящие поставки</span>}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Отгрузки - для оптовиков */}
|
||||
{user?.organization?.type === 'WHOLESALE' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'} text-left transition-all duration-200 text-xs ${
|
||||
isSuppliesActive
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Отгрузки" : ""}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Отгрузки</span>}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Перевозки - для логистов */}
|
||||
{user?.organization?.type === 'LOGIST' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'} text-left transition-all duration-200 text-xs ${
|
||||
isSuppliesActive
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Перевозки" : ""}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Перевозки</span>}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
@ -0,0 +1,280 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Package,
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
|
||||
// Мок данные для товаров
|
||||
const mockGoodsSupplies = [
|
||||
{
|
||||
id: "1",
|
||||
productName: "Смартфон iPhone 15",
|
||||
sku: "IPH15-128-BLK",
|
||||
seller: "TechStore LLC",
|
||||
quantity: 50,
|
||||
expectedDate: "2024-01-15",
|
||||
status: "planned",
|
||||
totalValue: 2500000,
|
||||
warehouse: "Склад А1",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
productName: "Ноутбук MacBook Air",
|
||||
sku: "MBA-M2-256-SLV",
|
||||
seller: "Apple Reseller",
|
||||
quantity: 25,
|
||||
expectedDate: "2024-01-12",
|
||||
status: "in-transit",
|
||||
totalValue: 3750000,
|
||||
warehouse: "Склад Б2",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
productName: "Наушники AirPods Pro",
|
||||
sku: "APP-2GEN-WHT",
|
||||
seller: "Audio World",
|
||||
quantity: 100,
|
||||
expectedDate: "2024-01-10",
|
||||
status: "delivered",
|
||||
totalValue: 2800000,
|
||||
warehouse: "Склад А1",
|
||||
},
|
||||
];
|
||||
|
||||
export function FulfillmentGoodsTab() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
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 getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
planned: {
|
||||
color: "text-blue-300 border-blue-400/30",
|
||||
label: "Запланировано",
|
||||
},
|
||||
"in-transit": {
|
||||
color: "text-yellow-300 border-yellow-400/30",
|
||||
label: "В пути",
|
||||
},
|
||||
delivered: {
|
||||
color: "text-green-300 border-green-400/30",
|
||||
label: "Доставлено",
|
||||
},
|
||||
"in-processing": {
|
||||
color: "text-purple-300 border-purple-400/30",
|
||||
label: "Обрабатывается",
|
||||
},
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredSupplies = mockGoodsSupplies.filter((supply) => {
|
||||
const matchesSearch =
|
||||
supply.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.sku.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.seller.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || supply.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const getTotalValue = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.totalValue, 0);
|
||||
};
|
||||
|
||||
const getTotalQuantity = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.quantity, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-4 p-4">
|
||||
{/* Статистика с кнопкой */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3 flex-1">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-blue-500/20 rounded">
|
||||
<Package className="h-3 w-3 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Поставок</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{filteredSupplies.length}
|
||||
</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-green-500/20 rounded">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Стоимость</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{formatCurrency(getTotalValue())}
|
||||
</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-purple-500/20 rounded">
|
||||
<AlertCircle className="h-3 w-3 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Товаров</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{getTotalQuantity()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white text-sm px-6 h-[60px] whitespace-nowrap"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Создать поставку
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Фильтры */}
|
||||
<div className="flex items-center gap-3">
|
||||
<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" />
|
||||
<Input
|
||||
placeholder="Поиск по товарам, SKU, селлерам..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="glass-input pl-10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="planned">Запланировано</option>
|
||||
<option value="in-transit">В пути</option>
|
||||
<option value="delivered">Доставлено</option>
|
||||
<option value="in-processing">Обрабатывается</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Список поставок */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto space-y-3">
|
||||
{filteredSupplies.map((supply) => (
|
||||
<Card
|
||||
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">
|
||||
<div>
|
||||
<p className="text-white/60">SKU</p>
|
||||
<p className="text-white font-mono">{supply.sku}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Селлер</p>
|
||||
<p className="text-white">{supply.seller}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Количество</p>
|
||||
<p className="text-white font-semibold">
|
||||
{supply.quantity} шт.
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Package, Wrench, RotateCcw } from "lucide-react";
|
||||
|
||||
// Импорты компонентов подкатегорий
|
||||
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
|
||||
import { SellerMaterialsTab } from "./seller-materials-tab";
|
||||
import { PvzReturnsTab } from "./pvz-returns-tab";
|
||||
|
||||
export function FulfillmentSuppliesTab() {
|
||||
const [activeTab, setActiveTab] = useState("goods");
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
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">
|
||||
<TabsTrigger
|
||||
value="goods"
|
||||
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" />
|
||||
Товары
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="materials"
|
||||
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" />
|
||||
Расходники селлеров
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="returns"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Возвраты с ПВЗ
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="goods" className="flex-1 overflow-hidden">
|
||||
<FulfillmentGoodsTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="materials" className="flex-1 overflow-hidden">
|
||||
<SellerMaterialsTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="returns" className="flex-1 overflow-hidden">
|
||||
<PvzReturnsTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,336 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
RotateCcw,
|
||||
Plus,
|
||||
Search,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
|
||||
// Мок данные для возвратов с ПВЗ
|
||||
const mockPvzReturns = [
|
||||
{
|
||||
id: "1",
|
||||
productName: "Смартфон Samsung Galaxy S23",
|
||||
sku: "SAM-S23-128-BLK",
|
||||
pvzAddress: "ул. Ленина, 15, ПВЗ №1234",
|
||||
returnDate: "2024-01-13",
|
||||
status: "collected",
|
||||
quantity: 3,
|
||||
reason: "Брак товара",
|
||||
estimatedValue: 150000,
|
||||
seller: "TechWorld",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
productName: "Кроссовки Nike Air Max",
|
||||
sku: "NIKE-AM-42-WHT",
|
||||
pvzAddress: "пр. Мира, 88, ПВЗ №5678",
|
||||
returnDate: "2024-01-12",
|
||||
status: "pending",
|
||||
quantity: 2,
|
||||
reason: "Не подошел размер",
|
||||
estimatedValue: 24000,
|
||||
seller: "SportsGear",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
productName: "Планшет iPad Air",
|
||||
sku: "IPAD-AIR-256-GRY",
|
||||
pvzAddress: "ул. Советская, 42, ПВЗ №9012",
|
||||
returnDate: "2024-01-11",
|
||||
status: "processed",
|
||||
quantity: 1,
|
||||
reason: "Передумал покупать",
|
||||
estimatedValue: 85000,
|
||||
seller: "AppleStore",
|
||||
},
|
||||
];
|
||||
|
||||
export function PvzReturnsTab() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
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 getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: {
|
||||
color: "text-yellow-300 border-yellow-400/30",
|
||||
label: "Ожидает сбора",
|
||||
},
|
||||
collected: {
|
||||
color: "text-blue-300 border-blue-400/30",
|
||||
label: "Собрано",
|
||||
},
|
||||
processed: {
|
||||
color: "text-green-300 border-green-400/30",
|
||||
label: "Обработано",
|
||||
},
|
||||
disposed: {
|
||||
color: "text-red-300 border-red-400/30",
|
||||
label: "Утилизировано",
|
||||
},
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.pending;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getReasonBadge = (reason: string) => {
|
||||
const reasonConfig = {
|
||||
"Брак товара": { color: "text-red-300 border-red-400/30" },
|
||||
"Не подошел размер": { color: "text-orange-300 border-orange-400/30" },
|
||||
"Передумал покупать": { color: "text-blue-300 border-blue-400/30" },
|
||||
Другое: { color: "text-gray-300 border-gray-400/30" },
|
||||
};
|
||||
|
||||
const config =
|
||||
reasonConfig[reason as keyof typeof reasonConfig] ||
|
||||
reasonConfig["Другое"];
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`glass-secondary ${config.color} text-xs`}
|
||||
>
|
||||
{reason}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredReturns = mockPvzReturns.filter((returnItem) => {
|
||||
const matchesSearch =
|
||||
returnItem.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
returnItem.sku.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
returnItem.pvzAddress.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
returnItem.seller.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || returnItem.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const getTotalValue = () => {
|
||||
return filteredReturns.reduce(
|
||||
(sum, returnItem) => sum + returnItem.estimatedValue,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const getTotalQuantity = () => {
|
||||
return filteredReturns.reduce(
|
||||
(sum, returnItem) => sum + returnItem.quantity,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const getPendingCount = () => {
|
||||
return filteredReturns.filter(
|
||||
(returnItem) => returnItem.status === "pending"
|
||||
).length;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-4 p-4">
|
||||
{/* Статистика с кнопкой */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 flex-1">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-blue-500/20 rounded">
|
||||
<RotateCcw className="h-3 w-3 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Возвратов</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{filteredReturns.length}
|
||||
</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-yellow-500/20 rounded">
|
||||
<AlertCircle className="h-3 w-3 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Ожидает сбора</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{getPendingCount()}
|
||||
</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-green-500/20 rounded">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Стоимость</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{formatCurrency(getTotalValue())}
|
||||
</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-purple-500/20 rounded">
|
||||
<MapPin className="h-3 w-3 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Товаров</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{getTotalQuantity()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white text-sm px-6 h-[60px] whitespace-nowrap"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Запланировать сбор
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Фильтры */}
|
||||
<div className="flex items-center gap-3">
|
||||
<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" />
|
||||
<Input
|
||||
placeholder="Поиск по товарам, ПВЗ, селлерам..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="glass-input pl-10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="pending">Ожидает сбора</option>
|
||||
<option value="collected">Собрано</option>
|
||||
<option value="processed">Обработано</option>
|
||||
<option value="disposed">Утилизировано</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Список возвратов */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto space-y-3">
|
||||
{filteredReturns.map((returnItem) => (
|
||||
<Card
|
||||
key={returnItem.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">
|
||||
{returnItem.productName}
|
||||
</h3>
|
||||
{getStatusBadge(returnItem.status)}
|
||||
{getReasonBadge(returnItem.reason)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-white/60">SKU</p>
|
||||
<p className="text-white font-mono">{returnItem.sku}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Селлер</p>
|
||||
<p className="text-white">{returnItem.seller}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Количество</p>
|
||||
<p className="text-white font-semibold">
|
||||
{returnItem.quantity} шт.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Дата возврата</p>
|
||||
<p className="text-white">
|
||||
{formatDate(returnItem.returnDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<p className="text-white/60 text-sm flex items-center gap-2">
|
||||
<MapPin className="h-3 w-3" />
|
||||
ПВЗ:{" "}
|
||||
<span className="text-white">
|
||||
{returnItem.pvzAddress}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<span className="text-white/60 text-sm">
|
||||
Оценочная стоимость:{" "}
|
||||
<span className="text-green-400 font-semibold">
|
||||
{formatCurrency(returnItem.estimatedValue)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +1,296 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Wrench,
|
||||
Plus,
|
||||
Search,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
|
||||
// Мок данные для расходников селлеров
|
||||
const mockSellerMaterials = [
|
||||
{
|
||||
id: "1",
|
||||
materialName: "Пакеты полиэтиленовые 30х40",
|
||||
seller: "PackStore LLC",
|
||||
category: "Упаковка",
|
||||
quantity: 10000,
|
||||
expectedDate: "2024-01-14",
|
||||
status: "planned",
|
||||
unitPrice: 2.5,
|
||||
totalValue: 25000,
|
||||
purpose: "Упаковка мелких товаров",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
materialName: "Скотч упаковочный прозрачный",
|
||||
seller: "Packaging Pro",
|
||||
category: "Упаковка",
|
||||
quantity: 500,
|
||||
expectedDate: "2024-01-11",
|
||||
status: "in-transit",
|
||||
unitPrice: 85,
|
||||
totalValue: 42500,
|
||||
purpose: "Заклейка коробок",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
materialName: "Этикетки штрих-код 58х40",
|
||||
seller: "LabelTech",
|
||||
category: "Маркировка",
|
||||
quantity: 50000,
|
||||
expectedDate: "2024-01-09",
|
||||
status: "delivered",
|
||||
unitPrice: 0.8,
|
||||
totalValue: 40000,
|
||||
purpose: "Маркировка товаров",
|
||||
},
|
||||
];
|
||||
|
||||
export function SellerMaterialsTab() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
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 getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
planned: {
|
||||
color: "text-blue-300 border-blue-400/30",
|
||||
label: "Запланировано",
|
||||
},
|
||||
"in-transit": {
|
||||
color: "text-yellow-300 border-yellow-400/30",
|
||||
label: "В пути",
|
||||
},
|
||||
delivered: {
|
||||
color: "text-green-300 border-green-400/30",
|
||||
label: "Доставлено",
|
||||
},
|
||||
"in-processing": {
|
||||
color: "text-purple-300 border-purple-400/30",
|
||||
label: "Обрабатывается",
|
||||
},
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredMaterials = mockSellerMaterials.filter((material) => {
|
||||
const matchesSearch =
|
||||
material.materialName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
material.seller.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
material.category.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || material.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const getTotalValue = () => {
|
||||
return filteredMaterials.reduce(
|
||||
(sum, material) => sum + material.totalValue,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const getTotalQuantity = () => {
|
||||
return filteredMaterials.reduce(
|
||||
(sum, material) => sum + material.quantity,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-4 p-4">
|
||||
{/* Статистика с кнопкой */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3 flex-1">
|
||||
<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">
|
||||
<Wrench 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">
|
||||
{filteredMaterials.length}
|
||||
</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-green-500/20 rounded">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Стоимость</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{formatCurrency(getTotalValue())}
|
||||
</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-purple-500/20 rounded">
|
||||
<AlertCircle className="h-3 w-3 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Единиц</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{getTotalQuantity().toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white text-sm px-6 h-[60px] whitespace-nowrap"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Заказать материалы
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Фильтры */}
|
||||
<div className="flex items-center gap-3">
|
||||
<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" />
|
||||
<Input
|
||||
placeholder="Поиск по материалам, селлерам..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="glass-input pl-10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="planned">Запланировано</option>
|
||||
<option value="in-transit">В пути</option>
|
||||
<option value="delivered">Доставлено</option>
|
||||
<option value="in-processing">Обрабатывается</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Список материалов */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto space-y-3">
|
||||
{filteredMaterials.map((material) => (
|
||||
<Card
|
||||
key={material.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">
|
||||
{material.materialName}
|
||||
</h3>
|
||||
{getStatusBadge(material.status)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-white/60">Селлер</p>
|
||||
<p className="text-white">{material.seller}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Категория</p>
|
||||
<p className="text-white">{material.category}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Количество</p>
|
||||
<p className="text-white font-semibold">
|
||||
{material.quantity.toLocaleString()} шт.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Ожидается</p>
|
||||
<p className="text-white">
|
||||
{formatDate(material.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">
|
||||
{formatCurrency(material.unitPrice)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-white/60">
|
||||
Общая стоимость:{" "}
|
||||
<span className="text-green-400 font-semibold">
|
||||
{formatCurrency(material.totalValue)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="text-white/60 text-xs">
|
||||
Назначение:{" "}
|
||||
<span className="text-white/80">{material.purpose}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ShoppingCart, Package } from "lucide-react";
|
||||
|
||||
// Импорты компонентов маркетплейсов
|
||||
import { WildberriesSuppliesTab } from "./wildberries-supplies-tab";
|
||||
import { OzonSuppliesTab } from "./ozon-supplies-tab";
|
||||
|
||||
export function MarketplaceSuppliesTab() {
|
||||
const [activeTab, setActiveTab] = useState("wildberries");
|
||||
|
||||
return (
|
||||
<div 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/10 backdrop-blur border-white/10 flex-shrink-0 h-10 mb-3 mx-4 mt-4">
|
||||
<TabsTrigger
|
||||
value="wildberries"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-2 text-xs"
|
||||
>
|
||||
<div className="w-3 h-3 bg-purple-500 rounded-sm flex items-center justify-center">
|
||||
<span className="text-white text-[8px] font-bold">W</span>
|
||||
</div>
|
||||
Wildberries
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="ozon"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-2 text-xs"
|
||||
>
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-sm flex items-center justify-center">
|
||||
<span className="text-white text-[8px] font-bold">O</span>
|
||||
</div>
|
||||
Ozon
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="wildberries" className="flex-1 overflow-hidden">
|
||||
<WildberriesSuppliesTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="ozon" className="flex-1 overflow-hidden">
|
||||
<OzonSuppliesTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,326 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Package,
|
||||
Plus,
|
||||
Search,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
|
||||
// Мок данные для поставок на Ozon
|
||||
const mockOzonSupplies = [
|
||||
{
|
||||
id: "1",
|
||||
supplyId: "OZ-SP-240113-001",
|
||||
warehouse: "Тверь",
|
||||
deliveryDate: "2024-01-16",
|
||||
status: "awaiting_packaging",
|
||||
totalItems: 120,
|
||||
totalBoxes: 10,
|
||||
estimatedValue: 380000,
|
||||
products: [
|
||||
{ name: "Телефон Samsung A54", quantity: 40, price: 6500 },
|
||||
{ name: "Чехол силиконовый", quantity: 80, price: 850 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
supplyId: "OZ-SP-240112-002",
|
||||
warehouse: "Казань",
|
||||
deliveryDate: "2024-01-15",
|
||||
status: "sent_to_delivery",
|
||||
totalItems: 75,
|
||||
totalBoxes: 6,
|
||||
estimatedValue: 295000,
|
||||
products: [
|
||||
{ name: "Наушники беспроводные", quantity: 25, price: 4200 },
|
||||
{ name: "Зарядное устройство", quantity: 50, price: 1800 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
supplyId: "OZ-SP-240111-003",
|
||||
warehouse: "Екатеринбург",
|
||||
deliveryDate: "2024-01-14",
|
||||
status: "delivered",
|
||||
totalItems: 180,
|
||||
totalBoxes: 14,
|
||||
estimatedValue: 520000,
|
||||
products: [
|
||||
{ name: "Планшет Xiaomi Pad", quantity: 60, price: 4800 },
|
||||
{ name: "Клавиатура беспроводная", quantity: 120, price: 1650 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function OzonSuppliesTab() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
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 getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
awaiting_packaging: {
|
||||
color: "text-blue-300 border-blue-400/30",
|
||||
label: "Ожидает упаковки",
|
||||
},
|
||||
sent_to_delivery: {
|
||||
color: "text-yellow-300 border-yellow-400/30",
|
||||
label: "Отправлена",
|
||||
},
|
||||
delivered: {
|
||||
color: "text-green-300 border-green-400/30",
|
||||
label: "Доставлена",
|
||||
},
|
||||
cancelled: { color: "text-red-300 border-red-400/30", label: "Отменена" },
|
||||
arbitration: {
|
||||
color: "text-orange-300 border-orange-400/30",
|
||||
label: "Арбитраж",
|
||||
},
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] ||
|
||||
statusConfig.awaiting_packaging;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredSupplies = mockOzonSupplies.filter((supply) => {
|
||||
const matchesSearch =
|
||||
supply.supplyId.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.warehouse.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.products.some((p) =>
|
||||
p.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || supply.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const getTotalValue = () => {
|
||||
return filteredSupplies.reduce(
|
||||
(sum, supply) => sum + supply.estimatedValue,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const getTotalItems = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.totalItems, 0);
|
||||
};
|
||||
|
||||
const getTotalBoxes = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.totalBoxes, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-4 p-4">
|
||||
{/* Статистика с кнопкой */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 flex-1">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-blue-500/20 rounded">
|
||||
<Package className="h-3 w-3 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Поставок</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{filteredSupplies.length}
|
||||
</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-green-500/20 rounded">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Стоимость</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{formatCurrency(getTotalValue())}
|
||||
</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-purple-500/20 rounded">
|
||||
<AlertCircle className="h-3 w-3 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Товаров</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{getTotalItems()}
|
||||
</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">
|
||||
<Package 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">
|
||||
{getTotalBoxes()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 text-white text-sm px-6 h-[60px] whitespace-nowrap"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Создать поставку
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Фильтры */}
|
||||
<div className="flex items-center gap-3">
|
||||
<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" />
|
||||
<Input
|
||||
placeholder="Поиск по ID поставки, складу, товарам..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="glass-input pl-10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="awaiting_packaging">Ожидает упаковки</option>
|
||||
<option value="sent_to_delivery">Отправлена</option>
|
||||
<option value="delivered">Доставлена</option>
|
||||
<option value="cancelled">Отменена</option>
|
||||
<option value="arbitration">Арбитраж</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Список поставок */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto space-y-3">
|
||||
{filteredSupplies.map((supply) => (
|
||||
<Card
|
||||
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-3">
|
||||
<div className="w-6 h-6 bg-blue-500 rounded flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">O</span>
|
||||
</div>
|
||||
<h3 className="text-white font-medium">
|
||||
{supply.supplyId}
|
||||
</h3>
|
||||
{getStatusBadge(supply.status)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm mb-3">
|
||||
<div>
|
||||
<p className="text-white/60">Склад Ozon</p>
|
||||
<p className="text-white">{supply.warehouse}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Дата доставки</p>
|
||||
<p className="text-white flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{formatDate(supply.deliveryDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Товаров / Коробок</p>
|
||||
<p className="text-white font-semibold">
|
||||
{supply.totalItems} / {supply.totalBoxes}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Стоимость</p>
|
||||
<p className="text-green-400 font-semibold">
|
||||
{formatCurrency(supply.estimatedValue)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Список товаров в поставке */}
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<p className="text-white/80 text-xs mb-2">
|
||||
Товары в поставке:
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{supply.products.map((product, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between text-xs"
|
||||
>
|
||||
<span className="text-white/70">{product.name}</span>
|
||||
<span className="text-white/60">
|
||||
{product.quantity} шт. ×{" "}
|
||||
{formatCurrency(product.price)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +1,322 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Package,
|
||||
Plus,
|
||||
Search,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
|
||||
// Мок данные для поставок на Wildberries
|
||||
const mockWbSupplies = [
|
||||
{
|
||||
id: "1",
|
||||
supplyId: "WB-SP-240113-001",
|
||||
warehouse: "Коледино",
|
||||
deliveryDate: "2024-01-15",
|
||||
status: "created",
|
||||
totalItems: 150,
|
||||
totalBoxes: 12,
|
||||
estimatedValue: 450000,
|
||||
products: [
|
||||
{ name: "Футболка базовая", quantity: 100, price: 1200 },
|
||||
{ name: "Джинсы классические", quantity: 50, price: 3500 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
supplyId: "WB-SP-240112-002",
|
||||
warehouse: "Электросталь",
|
||||
deliveryDate: "2024-01-14",
|
||||
status: "confirmed",
|
||||
totalItems: 85,
|
||||
totalBoxes: 8,
|
||||
estimatedValue: 320000,
|
||||
products: [
|
||||
{ name: "Кроссовки спортивные", quantity: 35, price: 4500 },
|
||||
{ name: "Рюкзак молодежный", quantity: 50, price: 2800 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
supplyId: "WB-SP-240111-003",
|
||||
warehouse: "Подольск",
|
||||
deliveryDate: "2024-01-13",
|
||||
status: "shipped",
|
||||
totalItems: 200,
|
||||
totalBoxes: 15,
|
||||
estimatedValue: 680000,
|
||||
products: [
|
||||
{ name: "Платье летнее", quantity: 80, price: 2200 },
|
||||
{ name: "Блузка офисная", quantity: 120, price: 3800 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function WildberriesSuppliesTab() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
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 getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
created: { color: "text-blue-300 border-blue-400/30", label: "Создана" },
|
||||
confirmed: {
|
||||
color: "text-yellow-300 border-yellow-400/30",
|
||||
label: "Подтверждена",
|
||||
},
|
||||
shipped: {
|
||||
color: "text-green-300 border-green-400/30",
|
||||
label: "Отправлена",
|
||||
},
|
||||
delivered: {
|
||||
color: "text-purple-300 border-purple-400/30",
|
||||
label: "Доставлена",
|
||||
},
|
||||
cancelled: { color: "text-red-300 border-red-400/30", label: "Отменена" },
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.created;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredSupplies = mockWbSupplies.filter((supply) => {
|
||||
const matchesSearch =
|
||||
supply.supplyId.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.warehouse.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.products.some((p) =>
|
||||
p.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || supply.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const getTotalValue = () => {
|
||||
return filteredSupplies.reduce(
|
||||
(sum, supply) => sum + supply.estimatedValue,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const getTotalItems = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.totalItems, 0);
|
||||
};
|
||||
|
||||
const getTotalBoxes = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.totalBoxes, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-4 p-4">
|
||||
{/* Статистика с кнопкой */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 flex-1">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-purple-500/20 rounded">
|
||||
<Package className="h-3 w-3 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Поставок</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{filteredSupplies.length}
|
||||
</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-green-500/20 rounded">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Стоимость</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{formatCurrency(getTotalValue())}
|
||||
</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-blue-500/20 rounded">
|
||||
<AlertCircle className="h-3 w-3 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Товаров</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{getTotalItems()}
|
||||
</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">
|
||||
<Package 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">
|
||||
{getTotalBoxes()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-sm px-6 h-[60px] whitespace-nowrap"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Создать поставку
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Фильтры */}
|
||||
<div className="flex items-center gap-3">
|
||||
<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" />
|
||||
<Input
|
||||
placeholder="Поиск по ID поставки, складу, товарам..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="glass-input pl-10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="created">Создана</option>
|
||||
<option value="confirmed">Подтверждена</option>
|
||||
<option value="shipped">Отправлена</option>
|
||||
<option value="delivered">Доставлена</option>
|
||||
<option value="cancelled">Отменена</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Список поставок */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto space-y-3">
|
||||
{filteredSupplies.map((supply) => (
|
||||
<Card
|
||||
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-3">
|
||||
<div className="w-6 h-6 bg-purple-500 rounded flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">W</span>
|
||||
</div>
|
||||
<h3 className="text-white font-medium">
|
||||
{supply.supplyId}
|
||||
</h3>
|
||||
{getStatusBadge(supply.status)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm mb-3">
|
||||
<div>
|
||||
<p className="text-white/60">Склад WB</p>
|
||||
<p className="text-white">{supply.warehouse}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Дата доставки</p>
|
||||
<p className="text-white flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{formatDate(supply.deliveryDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Товаров / Коробок</p>
|
||||
<p className="text-white font-semibold">
|
||||
{supply.totalItems} / {supply.totalBoxes}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Стоимость</p>
|
||||
<p className="text-green-400 font-semibold">
|
||||
{formatCurrency(supply.estimatedValue)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Список товаров в поставке */}
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<p className="text-white/80 text-xs mb-2">
|
||||
Товары в поставке:
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{supply.products.map((product, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between text-xs"
|
||||
>
|
||||
<span className="text-white/70">{product.name}</span>
|
||||
<span className="text-white/60">
|
||||
{product.quantity} шт. ×{" "}
|
||||
{formatCurrency(product.price)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
@ -167,26 +167,11 @@ export function MaterialsSuppliesTab() {
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-4 p-4">
|
||||
{/* Компактный заголовок */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wrench className="h-4 w-4 text-purple-400" />
|
||||
<span className="text-white font-medium text-sm">Расходники</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => window.location.href = '/fulfillment-supplies/materials/order'}
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-xs"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Заказать
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Компактная статистика */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Статистика с кнопкой заказа */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 flex-1">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-purple-500/20 rounded">
|
||||
<Wrench className="h-3 w-3 text-purple-400" />
|
||||
</div>
|
||||
@ -197,8 +182,8 @@ export function MaterialsSuppliesTab() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-green-500/20 rounded">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
@ -209,8 +194,8 @@ export function MaterialsSuppliesTab() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-blue-500/20 rounded">
|
||||
<AlertCircle className="h-3 w-3 text-blue-400" />
|
||||
</div>
|
||||
@ -221,8 +206,8 @@ export function MaterialsSuppliesTab() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Card className="glass-card p-3 h-[60px]">
|
||||
<div className="flex items-center space-x-2 h-full">
|
||||
<div className="p-1.5 bg-red-500/20 rounded">
|
||||
<Calendar className="h-3 w-3 text-red-400" />
|
||||
</div>
|
||||
@ -234,6 +219,17 @@ export function MaterialsSuppliesTab() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Кнопка заказа */}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => window.location.href = '/fulfillment-supplies/materials/order'}
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-sm px-6 h-[60px] whitespace-nowrap"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Заказать
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Компактный поиск и фильтры */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<div className="relative flex-1">
|
||||
|
122
src/components/fulfillment-supplies/supplies-dashboard.tsx
Normal file
122
src/components/fulfillment-supplies/supplies-dashboard.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import {
|
||||
Package,
|
||||
Wrench,
|
||||
Truck,
|
||||
ArrowLeftRight,
|
||||
Building,
|
||||
ShoppingCart,
|
||||
} from "lucide-react";
|
||||
|
||||
// Импорты компонентов
|
||||
import { MaterialsSuppliesTab } from "./materials-supplies/materials-supplies-tab";
|
||||
import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab";
|
||||
import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab";
|
||||
|
||||
export function SuppliesDashboard() {
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
const [mainTab, setMainTab] = useState("goods");
|
||||
const [goodsSubTab, setGoodsSubTab] = useState("fulfillment");
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<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="mb-4">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Поставки</h1>
|
||||
<p className="text-white/60 text-sm">
|
||||
Управление поставками товаров и расходников
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Основная навигация */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs
|
||||
value={mainTab}
|
||||
onValueChange={setMainTab}
|
||||
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-12 mb-4">
|
||||
<TabsTrigger
|
||||
value="goods"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<Package className="h-4 w-4" />
|
||||
Поставки товаров
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="materials"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
Поставки расходников
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Поставки товаров */}
|
||||
<TabsContent value="goods" className="flex-1 overflow-hidden">
|
||||
<Tabs
|
||||
value={goodsSubTab}
|
||||
onValueChange={setGoodsSubTab}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2 bg-white/10 backdrop-blur border-white/10 flex-shrink-0 h-10 mb-3">
|
||||
<TabsTrigger
|
||||
value="fulfillment"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-2 text-xs"
|
||||
>
|
||||
<Building className="h-3 w-3" />
|
||||
Наш фулфилмент
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="marketplaces"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-2 text-xs"
|
||||
>
|
||||
<ShoppingCart className="h-3 w-3" />
|
||||
Маркетплейсы
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
value="fulfillment"
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
<Card className="glass-card h-full overflow-hidden p-0">
|
||||
<FulfillmentSuppliesTab />
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="marketplaces"
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
<Card className="glass-card h-full overflow-hidden p-0">
|
||||
<MarketplaceSuppliesTab />
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</TabsContent>
|
||||
|
||||
{/* Поставки расходников */}
|
||||
<TabsContent value="materials" className="flex-1 overflow-hidden">
|
||||
<Card className="glass-card h-full overflow-hidden p-0">
|
||||
<MaterialsSuppliesTab />
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
285
src/components/logistics/logistics-dashboard.tsx
Normal file
285
src/components/logistics/logistics-dashboard.tsx
Normal file
@ -0,0 +1,285 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import {
|
||||
Truck,
|
||||
Plus,
|
||||
MapPin,
|
||||
Clock,
|
||||
Package,
|
||||
TrendingUp,
|
||||
AlertTriangle,
|
||||
Navigation,
|
||||
} from "lucide-react";
|
||||
|
||||
// Мок данные для перевозок
|
||||
const mockLogistics = [
|
||||
{
|
||||
id: "1",
|
||||
routeNumber: "LOG-001",
|
||||
from: "Садовод",
|
||||
fromAddress: "Москва, 14-й км МКАД",
|
||||
to: "SFERAV Logistics",
|
||||
toAddress: "Москва, ул. Складская, 15",
|
||||
status: "in_transit",
|
||||
distance: "45 км",
|
||||
estimatedTime: "1 ч 30 мин",
|
||||
cargo: "Смартфоны (50 шт.)",
|
||||
price: 15000,
|
||||
createdDate: "2024-01-10",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
routeNumber: "LOG-002",
|
||||
from: "SFERAV Logistics",
|
||||
fromAddress: "Москва, ул. Складская, 15",
|
||||
to: "Коледино WB",
|
||||
toAddress: "МО, г. Подольск, Коледино",
|
||||
status: "delivered",
|
||||
distance: "62 км",
|
||||
estimatedTime: "2 ч 15 мин",
|
||||
cargo: "Одежда (120 шт.)",
|
||||
price: 22000,
|
||||
createdDate: "2024-01-09",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
routeNumber: "LOG-003",
|
||||
from: "Тверь Ozon",
|
||||
fromAddress: "г. Тверь, ул. Складская, 88",
|
||||
to: "SFERAV Logistics",
|
||||
toAddress: "Москва, ул. Складская, 15",
|
||||
status: "planned",
|
||||
distance: "178 км",
|
||||
estimatedTime: "3 ч 45 мин",
|
||||
cargo: "Электроника (75 шт.)",
|
||||
price: 35000,
|
||||
createdDate: "2024-01-11",
|
||||
},
|
||||
];
|
||||
|
||||
export function LogisticsDashboard() {
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
|
||||
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 getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
planned: {
|
||||
color: "text-blue-300 border-blue-400/30",
|
||||
label: "Запланировано",
|
||||
},
|
||||
in_transit: {
|
||||
color: "text-yellow-300 border-yellow-400/30",
|
||||
label: "В пути",
|
||||
},
|
||||
delivered: {
|
||||
color: "text-green-300 border-green-400/30",
|
||||
label: "Доставлено",
|
||||
},
|
||||
cancelled: { color: "text-red-300 border-red-400/30", label: "Отменено" },
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getTotalRevenue = () => {
|
||||
return mockLogistics.reduce((sum, route) => sum + route.price, 0);
|
||||
};
|
||||
|
||||
const getInTransitCount = () => {
|
||||
return mockLogistics.filter((route) => route.status === "in_transit")
|
||||
.length;
|
||||
};
|
||||
|
||||
const getDeliveredCount = () => {
|
||||
return mockLogistics.filter((route) => route.status === "delivered").length;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main
|
||||
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
|
||||
>
|
||||
<div className="p-8">
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Перевозки</h1>
|
||||
<p className="text-white/60">
|
||||
Управление логистическими маршрутами
|
||||
</p>
|
||||
</div>
|
||||
<Button className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white shadow-lg">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Создать маршрут
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Статистика */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-3 bg-blue-500/20 rounded-lg">
|
||||
<Truck className="h-6 w-6 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Всего маршрутов</p>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{mockLogistics.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-3 bg-yellow-500/20 rounded-lg">
|
||||
<Navigation className="h-6 w-6 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">В пути</p>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{getInTransitCount()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-3 bg-green-500/20 rounded-lg">
|
||||
<Package className="h-6 w-6 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Доставлено</p>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{getDeliveredCount()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-3 bg-purple-500/20 rounded-lg">
|
||||
<TrendingUp className="h-6 w-6 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Выручка</p>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{formatCurrency(getTotalRevenue())}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Список маршрутов */}
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-semibold text-white mb-4">
|
||||
Активные маршруты
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{mockLogistics.map((route) => (
|
||||
<Card
|
||||
key={route.id}
|
||||
className="glass-card p-4 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h3 className="text-white font-semibold text-lg">
|
||||
{route.routeNumber}
|
||||
</h3>
|
||||
{getStatusBadge(route.status)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-green-400" />
|
||||
<div>
|
||||
<p className="text-white font-medium">
|
||||
{route.from}
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">
|
||||
{route.fromAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-red-400" />
|
||||
<div>
|
||||
<p className="text-white font-medium">
|
||||
{route.to}
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">
|
||||
{route.toAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-white">{route.cargo}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-yellow-400" />
|
||||
<span className="text-white">
|
||||
{route.distance} • {route.estimatedTime}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/60 text-sm">
|
||||
Создано: {formatDate(route.createdDate)}
|
||||
</span>
|
||||
<span className="text-green-400 font-semibold text-lg">
|
||||
{formatCurrency(route.price)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user