Добавлена логика маршрутизации для поставок в зависимости от типа организации пользователя. Обновлены компоненты боковой панели и страницы создания поставки: реализован поиск оптовиков, улучшена фильтрация товаров и адаптация данных оптовиков. Убраны неиспользуемые поля и улучшен интерфейс отображения информации о товарах и оптовиках.
This commit is contained in:
10
src/app/fulfillment-supplies/page.tsx
Normal file
10
src/app/fulfillment-supplies/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { AuthGuard } from "@/components/auth-guard"
|
||||
import { FulfillmentSuppliesDashboard } from "@/components/fulfillment-supplies/fulfillment-supplies-dashboard"
|
||||
|
||||
export default function FulfillmentSuppliesPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<FulfillmentSuppliesDashboard />
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
@ -83,7 +83,12 @@ export function Sidebar() {
|
||||
}
|
||||
|
||||
const handleSuppliesClick = () => {
|
||||
router.push('/supplies')
|
||||
// Для фулфилмент кабинетов используем новый роут
|
||||
if (user?.organization?.type === 'FULFILLMENT') {
|
||||
router.push('/fulfillment-supplies')
|
||||
} else {
|
||||
router.push('/supplies')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePartnersClick = () => {
|
||||
@ -98,7 +103,7 @@ export function Sidebar() {
|
||||
const isServicesActive = pathname.startsWith('/services')
|
||||
const isWarehouseActive = pathname.startsWith('/warehouse')
|
||||
const isEmployeesActive = pathname.startsWith('/employees')
|
||||
const isSuppliesActive = pathname.startsWith('/supplies')
|
||||
const isSuppliesActive = pathname.startsWith('/supplies') || pathname.startsWith('/fulfillment-supplies')
|
||||
const isPartnersActive = pathname.startsWith('/partners')
|
||||
|
||||
return (
|
||||
@ -265,8 +270,8 @@ export function Sidebar() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Поставки - только для селлеров */}
|
||||
{user?.organization?.type === 'SELLER' && (
|
||||
{/* Поставки - для селлеров и фулфилмент */}
|
||||
{(user?.organization?.type === 'SELLER' || 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 ${
|
||||
|
@ -0,0 +1,60 @@
|
||||
"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, Truck, Wrench, ArrowLeftRight } from 'lucide-react'
|
||||
|
||||
// Импорты компонентов подразделов
|
||||
import { GoodsSuppliesTab } from './goods-supplies/goods-supplies-tab'
|
||||
import { MaterialsSuppliesTab } from './materials-supplies/materials-supplies-tab'
|
||||
|
||||
export function FulfillmentSuppliesDashboard() {
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
const [activeTab, setActiveTab] = useState('goods')
|
||||
|
||||
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="flex-1 overflow-hidden">
|
||||
<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">
|
||||
<TabsTrigger
|
||||
value="goods"
|
||||
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" />
|
||||
Товары
|
||||
</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-sm"
|
||||
>
|
||||
<Wrench className="h-3 w-3" />
|
||||
Расходники
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="goods" className="flex-1 overflow-hidden mt-3">
|
||||
<Card className="glass-card h-full overflow-hidden p-0">
|
||||
<GoodsSuppliesTab />
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="materials" className="flex-1 overflow-hidden mt-3">
|
||||
<Card className="glass-card h-full overflow-hidden p-0">
|
||||
<MaterialsSuppliesTab />
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,297 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Package, Wrench, RotateCcw, Plus, Calendar, TrendingUp, AlertCircle, Building2 } from 'lucide-react'
|
||||
|
||||
interface SupplyItem {
|
||||
id: string
|
||||
name: string
|
||||
type: 'product' | 'materials' | 'return'
|
||||
category: string
|
||||
quantity: number
|
||||
status: 'planned' | 'in-transit' | 'delivered' | 'processing'
|
||||
date: string
|
||||
supplier: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
const mockSupplies: SupplyItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Смартфоны iPhone 15',
|
||||
type: 'product',
|
||||
category: 'Электроника',
|
||||
quantity: 50,
|
||||
status: 'delivered',
|
||||
date: '2024-01-15',
|
||||
supplier: 'ООО "ТехноСнаб"',
|
||||
amount: 3750000
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Упаковочные коробки',
|
||||
type: 'materials',
|
||||
category: 'Упаковка',
|
||||
quantity: 1000,
|
||||
status: 'in-transit',
|
||||
date: '2024-01-18',
|
||||
supplier: 'ИП Селлер Один',
|
||||
amount: 25000
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Товары с брагом (ПВЗ №5)',
|
||||
type: 'return',
|
||||
category: 'Возврат',
|
||||
quantity: 15,
|
||||
status: 'processing',
|
||||
date: '2024-01-20',
|
||||
supplier: 'ПВЗ Москва-5',
|
||||
amount: 185000
|
||||
}
|
||||
]
|
||||
|
||||
export function FulfillmentSuppliesTab() {
|
||||
const [activeFilter, setActiveFilter] = useState<'all' | 'product' | 'materials' | 'return'>('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: { variant: 'outline' as const, color: 'text-blue-300 border-blue-400/30', label: 'Запланировано' },
|
||||
'in-transit': { variant: 'outline' as const, color: 'text-yellow-300 border-yellow-400/30', label: 'В пути' },
|
||||
delivered: { variant: 'outline' as const, color: 'text-green-300 border-green-400/30', label: 'Доставлено' },
|
||||
processing: { variant: 'outline' as const, color: 'text-orange-300 border-orange-400/30', label: 'Обработка' }
|
||||
}
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.planned
|
||||
|
||||
return (
|
||||
<Badge variant={config.variant} className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'product':
|
||||
return <Package className="h-4 w-4 text-blue-400" />
|
||||
case 'materials':
|
||||
return <Wrench className="h-4 w-4 text-purple-400" />
|
||||
case 'return':
|
||||
return <RotateCcw className="h-4 w-4 text-orange-400" />
|
||||
default:
|
||||
return <Package className="h-4 w-4 text-gray-400" />
|
||||
}
|
||||
}
|
||||
|
||||
const filteredSupplies = activeFilter === 'all'
|
||||
? mockSupplies
|
||||
: mockSupplies.filter(supply => supply.type === activeFilter)
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.amount, 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">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-white font-medium text-sm">Фулфилмент</span>
|
||||
</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-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="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">
|
||||
<div className="flex items-center space-x-2">
|
||||
<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(getTotalAmount())}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<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>
|
||||
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-1.5 bg-yellow-500/20 rounded">
|
||||
<Calendar 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">
|
||||
{filteredSupplies.filter(s => s.status === 'in-transit').length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Компактные фильтры */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant={activeFilter === 'all' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveFilter('all')}
|
||||
className={`h-7 px-2 text-xs ${activeFilter === 'all' ? 'bg-white/20 text-white' : 'text-white/70 hover:bg-white/10'}`}
|
||||
>
|
||||
Все
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeFilter === 'product' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveFilter('product')}
|
||||
className={`h-7 px-2 text-xs ${activeFilter === 'product' ? 'bg-white/20 text-white' : 'text-white/70 hover:bg-white/10'} flex items-center gap-1`}
|
||||
>
|
||||
<Package className="h-3 w-3" />
|
||||
Товары
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeFilter === 'materials' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveFilter('materials')}
|
||||
className={`h-7 px-2 text-xs ${activeFilter === 'materials' ? 'bg-white/20 text-white' : 'text-white/70 hover:bg-white/10'} flex items-center gap-1`}
|
||||
>
|
||||
<Wrench className="h-3 w-3" />
|
||||
Расходники
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeFilter === 'return' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveFilter('return')}
|
||||
className={`h-7 px-2 text-xs ${activeFilter === 'return' ? 'bg-white/20 text-white' : 'text-white/70 hover:bg-white/10'} flex items-center gap-1`}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Возвраты
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компактная таблица */}
|
||||
<Card className="glass-card flex-1 overflow-hidden">
|
||||
<div className="p-3 h-full flex flex-col">
|
||||
<div className="overflow-x-auto flex-1">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/20">
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Тип</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Наименование</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Категория</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Кол-во</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Поставщик</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Дата</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Сумма</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredSupplies.map((supply) => (
|
||||
<tr key={supply.id} className="border-b border-white/10 hover:bg-white/5 transition-colors">
|
||||
<td className="p-2">
|
||||
<div className="flex items-center justify-center">
|
||||
{getTypeIcon(supply.type)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white font-medium text-sm">{supply.name}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white/80 text-sm">{supply.category}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white font-semibold text-sm">{supply.quantity}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white/80 text-sm">{supply.supplier}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white/80 text-sm">{formatDate(supply.date)}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white font-semibold text-sm">{formatCurrency(supply.amount)}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{getStatusBadge(supply.status)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredSupplies.length === 0 && (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<Package className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">Поставки не найдены</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Измените фильтр или создайте новую поставку
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Building2, ShoppingCart } from 'lucide-react'
|
||||
|
||||
// Импорты компонентов подразделов
|
||||
import { FulfillmentSuppliesTab } from './fulfillment-supplies-tab'
|
||||
import { MarketplaceSuppliesTab } from './marketplace-supplies-tab'
|
||||
|
||||
export function GoodsSuppliesTab() {
|
||||
const [activeSubTab, setActiveSubTab] = useState('fulfillment')
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4">
|
||||
{/* Подвкладки */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs value={activeSubTab} onValueChange={setActiveSubTab} 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 mb-3 h-9">
|
||||
<TabsTrigger
|
||||
value="fulfillment"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-sm"
|
||||
>
|
||||
<Building2 className="h-3 w-3" />
|
||||
Фулфилмент
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="marketplace"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-sm"
|
||||
>
|
||||
<ShoppingCart className="h-3 w-3" />
|
||||
Маркетплейсы
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="fulfillment" className="flex-1 overflow-hidden">
|
||||
<FulfillmentSuppliesTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="marketplace" className="flex-1 overflow-hidden">
|
||||
<MarketplaceSuppliesTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ShoppingCart, Package, Plus, Calendar, TrendingUp, AlertCircle, Building2 } from 'lucide-react'
|
||||
|
||||
interface MarketplaceSupply {
|
||||
id: string
|
||||
name: string
|
||||
marketplace: 'wildberries' | 'ozon'
|
||||
category: string
|
||||
quantity: number
|
||||
status: 'planned' | 'in-transit' | 'delivered' | 'accepted'
|
||||
date: string
|
||||
warehouse: string
|
||||
amount: number
|
||||
sku: string
|
||||
}
|
||||
|
||||
const mockMarketplaceSupplies: MarketplaceSupply[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Наушники AirPods Pro',
|
||||
marketplace: 'wildberries',
|
||||
category: 'Аудио',
|
||||
quantity: 30,
|
||||
status: 'delivered',
|
||||
date: '2024-01-20',
|
||||
warehouse: 'WB Подольск',
|
||||
amount: 750000,
|
||||
sku: 'APL-AP-PRO2'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Смарт часы Apple Watch',
|
||||
marketplace: 'ozon',
|
||||
category: 'Электроника',
|
||||
quantity: 25,
|
||||
status: 'in-transit',
|
||||
date: '2024-01-22',
|
||||
warehouse: 'Ozon Тверь',
|
||||
amount: 1250000,
|
||||
sku: 'APL-AW-S9'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Зарядные устройства',
|
||||
marketplace: 'wildberries',
|
||||
category: 'Аксессуары',
|
||||
quantity: 100,
|
||||
status: 'accepted',
|
||||
date: '2024-01-18',
|
||||
warehouse: 'WB Электросталь',
|
||||
amount: 350000,
|
||||
sku: 'ACC-CHG-20W'
|
||||
}
|
||||
]
|
||||
|
||||
export function MarketplaceSuppliesTab() {
|
||||
const [activeMarketplace, setActiveMarketplace] = useState<'all' | 'wildberries' | 'ozon'>('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: { variant: 'outline' as const, color: 'text-blue-300 border-blue-400/30', label: 'Запланировано' },
|
||||
'in-transit': { variant: 'outline' as const, color: 'text-yellow-300 border-yellow-400/30', label: 'В пути' },
|
||||
delivered: { variant: 'outline' as const, color: 'text-green-300 border-green-400/30', label: 'Доставлено' },
|
||||
accepted: { variant: 'outline' as const, color: 'text-purple-300 border-purple-400/30', label: 'Принято' }
|
||||
}
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.planned
|
||||
|
||||
return (
|
||||
<Badge variant={config.variant} className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
const getMarketplaceBadge = (marketplace: string) => {
|
||||
if (marketplace === 'wildberries') {
|
||||
return (
|
||||
<Badge variant="outline" className="glass-secondary text-purple-300 border-purple-400/30">
|
||||
Wildberries
|
||||
</Badge>
|
||||
)
|
||||
} else if (marketplace === 'ozon') {
|
||||
return (
|
||||
<Badge variant="outline" className="glass-secondary text-blue-300 border-blue-400/30">
|
||||
Ozon
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const filteredSupplies = activeMarketplace === 'all'
|
||||
? mockMarketplaceSupplies
|
||||
: mockMarketplaceSupplies.filter(supply => supply.marketplace === activeMarketplace)
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.amount, 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">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4 text-purple-400" />
|
||||
<span className="text-white font-medium text-sm">Маркетплейсы</span>
|
||||
</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-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="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-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-sm">Общая сумма</p>
|
||||
<p className="text-xl font-bold text-white">{formatCurrency(getTotalAmount())}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-blue-500/20 rounded-lg">
|
||||
<AlertCircle className="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Единиц товара</p>
|
||||
<p className="text-xl font-bold text-white">{getTotalQuantity()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card 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-sm">В пути</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{filteredSupplies.filter(s => s.status === 'in-transit').length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Фильтры по маркетплейсу */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-white/60 text-sm">Маркетплейс:</span>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant={activeMarketplace === 'all' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveMarketplace('all')}
|
||||
className={`${activeMarketplace === 'all' ? 'bg-white/20 text-white' : 'text-white/70 hover:bg-white/10'}`}
|
||||
>
|
||||
Все
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeMarketplace === 'wildberries' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveMarketplace('wildberries')}
|
||||
className={`${activeMarketplace === 'wildberries' ? 'bg-white/20 text-white' : 'text-white/70 hover:bg-white/10'} flex items-center gap-1`}
|
||||
>
|
||||
<div className="w-3 h-3 bg-purple-400 rounded-full"></div>
|
||||
Wildberries
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeMarketplace === 'ozon' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveMarketplace('ozon')}
|
||||
className={`${activeMarketplace === 'ozon' ? 'bg-white/20 text-white' : 'text-white/70 hover:bg-white/10'} flex items-center gap-1`}
|
||||
>
|
||||
<div className="w-3 h-3 bg-blue-400 rounded-full"></div>
|
||||
Ozon
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Таблица поставок */}
|
||||
<Card className="glass-card flex-1 overflow-hidden">
|
||||
<div className="p-6 h-full flex flex-col">
|
||||
<div className="overflow-x-auto flex-1">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-white/20">
|
||||
<th className="text-left p-3 text-white font-semibold">Маркетплейс</th>
|
||||
<th className="text-left p-3 text-white font-semibold">Наименование</th>
|
||||
<th className="text-left p-3 text-white font-semibold">SKU</th>
|
||||
<th className="text-left p-3 text-white font-semibold">Категория</th>
|
||||
<th className="text-left p-3 text-white font-semibold">Количество</th>
|
||||
<th className="text-left p-3 text-white font-semibold">Склад</th>
|
||||
<th className="text-left p-3 text-white font-semibold">Дата</th>
|
||||
<th className="text-left p-3 text-white font-semibold">Сумма</th>
|
||||
<th className="text-left p-3 text-white font-semibold">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredSupplies.map((supply) => (
|
||||
<tr key={supply.id} className="border-b border-white/10 hover:bg-white/5 transition-colors">
|
||||
<td className="p-3">
|
||||
{getMarketplaceBadge(supply.marketplace)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="text-white font-medium">{supply.name}</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="text-white/80 font-mono text-sm">{supply.sku}</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="text-white/80">{supply.category}</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="text-white font-semibold">{supply.quantity}</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="text-white/80">{supply.warehouse}</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="text-white/80">{formatDate(supply.date)}</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className="text-white font-semibold">{formatCurrency(supply.amount)}</span>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{getStatusBadge(supply.status)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredSupplies.length === 0 && (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<ShoppingCart className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">Поставки не найдены</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Измените фильтр или создайте новую поставку
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,347 @@
|
||||
"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 { Input } from '@/components/ui/input'
|
||||
import { Wrench, Plus, Calendar, TrendingUp, AlertCircle, Search, Filter } from 'lucide-react'
|
||||
|
||||
interface MaterialSupply {
|
||||
id: string
|
||||
name: string
|
||||
category: string
|
||||
quantity: number
|
||||
unit: string
|
||||
status: 'planned' | 'in-transit' | 'delivered' | 'in-stock'
|
||||
date: string
|
||||
supplier: string
|
||||
amount: number
|
||||
description?: string
|
||||
minStock: number
|
||||
currentStock: number
|
||||
}
|
||||
|
||||
const mockMaterialSupplies: MaterialSupply[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Упаковочные коробки 30x20x10',
|
||||
category: 'Упаковка',
|
||||
quantity: 1000,
|
||||
unit: 'шт',
|
||||
status: 'delivered',
|
||||
date: '2024-01-15',
|
||||
supplier: 'ООО "УпакСервис"',
|
||||
amount: 50000,
|
||||
description: 'Картонные коробки для мелких товаров',
|
||||
minStock: 200,
|
||||
currentStock: 350
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Пузырчатая пленка',
|
||||
category: 'Защитная упаковка',
|
||||
quantity: 500,
|
||||
unit: 'м²',
|
||||
status: 'in-transit',
|
||||
date: '2024-01-20',
|
||||
supplier: 'ИП Петров А.В.',
|
||||
amount: 25000,
|
||||
description: 'Пленка для защиты хрупких товаров',
|
||||
minStock: 100,
|
||||
currentStock: 80
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Скотч упаковочный прозрачный',
|
||||
category: 'Клейкая лента',
|
||||
quantity: 200,
|
||||
unit: 'рул',
|
||||
status: 'planned',
|
||||
date: '2024-01-25',
|
||||
supplier: 'ООО "КлейТех"',
|
||||
amount: 15000,
|
||||
description: 'Прозрачный скотч 48мм x 66м',
|
||||
minStock: 50,
|
||||
currentStock: 25
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Этикетки самоклеющиеся',
|
||||
category: 'Маркировка',
|
||||
quantity: 10000,
|
||||
unit: 'шт',
|
||||
status: 'in-stock',
|
||||
date: '2024-01-10',
|
||||
supplier: 'ООО "ЛейблПринт"',
|
||||
amount: 30000,
|
||||
description: 'Белые этикетки 100x70мм',
|
||||
minStock: 2000,
|
||||
currentStock: 3500
|
||||
}
|
||||
]
|
||||
|
||||
export function MaterialsSuppliesTab() {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('all')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('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: { variant: 'outline' as const, color: 'text-blue-300 border-blue-400/30', label: 'Запланировано' },
|
||||
'in-transit': { variant: 'outline' as const, color: 'text-yellow-300 border-yellow-400/30', label: 'В пути' },
|
||||
delivered: { variant: 'outline' as const, color: 'text-green-300 border-green-400/30', label: 'Доставлено' },
|
||||
'in-stock': { variant: 'outline' as const, color: 'text-purple-300 border-purple-400/30', label: 'На складе' }
|
||||
}
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.planned
|
||||
|
||||
return (
|
||||
<Badge variant={config.variant} className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
const getStockStatusBadge = (currentStock: number, minStock: number) => {
|
||||
if (currentStock <= minStock) {
|
||||
return (
|
||||
<Badge variant="outline" className="glass-secondary text-red-300 border-red-400/30">
|
||||
Низкий остаток
|
||||
</Badge>
|
||||
)
|
||||
} else if (currentStock <= minStock * 1.5) {
|
||||
return (
|
||||
<Badge variant="outline" className="glass-secondary text-yellow-300 border-yellow-400/30">
|
||||
Требует заказа
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Badge variant="outline" className="glass-secondary text-green-300 border-green-400/30">
|
||||
В норме
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
const filteredSupplies = mockMaterialSupplies.filter(supply => {
|
||||
const matchesSearch = supply.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.category.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
supply.supplier.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
||||
const matchesCategory = categoryFilter === 'all' || supply.category === categoryFilter
|
||||
const matchesStatus = statusFilter === 'all' || supply.status === statusFilter
|
||||
|
||||
return matchesSearch && matchesCategory && matchesStatus
|
||||
})
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.amount, 0)
|
||||
}
|
||||
|
||||
const getTotalQuantity = () => {
|
||||
return filteredSupplies.reduce((sum, supply) => sum + supply.quantity, 0)
|
||||
}
|
||||
|
||||
const getLowStockCount = () => {
|
||||
return mockMaterialSupplies.filter(supply => supply.currentStock <= supply.minStock).length
|
||||
}
|
||||
|
||||
const categories = Array.from(new Set(mockMaterialSupplies.map(supply => supply.category)))
|
||||
|
||||
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"
|
||||
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="p-1.5 bg-purple-500/20 rounded">
|
||||
<Wrench 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">
|
||||
<div className="flex items-center space-x-2">
|
||||
<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(getTotalAmount())}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<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">{getTotalQuantity()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-1.5 bg-red-500/20 rounded">
|
||||
<Calendar className="h-3 w-3 text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Низкий остаток</p>
|
||||
<p className="text-lg font-bold text-white">{getLowStockCount()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Компактный поиск и фильтры */}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-2 h-3 w-3 text-white/40" />
|
||||
<Input
|
||||
placeholder="Поиск..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-7 h-8 text-sm glass-input text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="px-2 py-1 h-8 bg-white/5 border border-white/20 rounded text-white text-xs focus:outline-none focus:ring-1 focus:ring-purple-500"
|
||||
>
|
||||
<option value="all">Все категории</option>
|
||||
{categories.map(category => (
|
||||
<option key={category} value={category}>{category}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-2 py-1 h-8 bg-white/5 border border-white/20 rounded text-white text-xs focus:outline-none focus:ring-1 focus:ring-purple-500"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="planned">Запланировано</option>
|
||||
<option value="in-transit">В пути</option>
|
||||
<option value="delivered">Доставлено</option>
|
||||
<option value="in-stock">На складе</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компактная таблица расходников */}
|
||||
<Card className="glass-card flex-1 overflow-hidden">
|
||||
<div className="p-3 h-full flex flex-col">
|
||||
<div className="overflow-x-auto flex-1">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/20">
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Наименование</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Категория</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Кол-во</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Остаток</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Поставщик</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Дата</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Сумма</th>
|
||||
<th className="text-left p-2 text-white font-medium text-xs">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredSupplies.map((supply) => (
|
||||
<tr key={supply.id} className="border-b border-white/10 hover:bg-white/5 transition-colors">
|
||||
<td className="p-2">
|
||||
<div>
|
||||
<span className="text-white font-medium text-sm">{supply.name}</span>
|
||||
{supply.description && (
|
||||
<p className="text-white/60 text-xs mt-0.5">{supply.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white/80 text-sm">{supply.category}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white font-semibold text-sm">{supply.quantity} {supply.unit}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-white font-semibold text-sm">{supply.currentStock} {supply.unit}</span>
|
||||
{getStockStatusBadge(supply.currentStock, supply.minStock)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white/80 text-sm">{supply.supplier}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white/80 text-sm">{formatDate(supply.date)}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<span className="text-white font-semibold text-sm">{formatCurrency(supply.amount)}</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{getStatusBadge(supply.status)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredSupplies.length === 0 && (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<Wrench className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">Расходники не найдены</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Попробуйте изменить параметры поиска или создать новый заказ
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS } from '@/graphql/queries'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ShoppingCart,
|
||||
@ -22,7 +25,8 @@ import {
|
||||
Zap,
|
||||
Heart,
|
||||
Eye,
|
||||
ShoppingBag
|
||||
ShoppingBag,
|
||||
Search
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
@ -221,10 +225,38 @@ const mockProducts: WholesalerProduct[] = [
|
||||
|
||||
export function CreateSupplyPage() {
|
||||
const router = useRouter()
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null)
|
||||
const [selectedWholesaler, setSelectedWholesaler] = useState<WholesalerForCreation | null>(null)
|
||||
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>([])
|
||||
const [showSummary, setShowSummary] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// Загружаем контрагентов-оптовиков
|
||||
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
|
||||
|
||||
// Загружаем товары для выбранного оптовика
|
||||
const { data: productsData, loading: productsLoading } = useQuery(GET_ALL_PRODUCTS, {
|
||||
skip: !selectedWholesaler,
|
||||
variables: { search: null, category: null }
|
||||
})
|
||||
|
||||
// Фильтруем только оптовиков
|
||||
const wholesalers = (counterpartiesData?.myCounterparties || []).filter((org: { type: string }) => org.type === 'WHOLESALE')
|
||||
|
||||
// Фильтруем оптовиков по поисковому запросу
|
||||
const filteredWholesalers = wholesalers.filter((wholesaler: { name?: string; fullName?: string; inn?: string }) =>
|
||||
wholesaler.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
wholesaler.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
wholesaler.inn?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
// Фильтруем товары по выбранному оптовику
|
||||
const wholesalerProducts = selectedWholesaler
|
||||
? (productsData?.allProducts || []).filter((product: { organization: { id: string } }) =>
|
||||
product.organization.id === selectedWholesaler.id
|
||||
)
|
||||
: []
|
||||
|
||||
// Автоматически показываем корзину если в ней есть товары и мы на этапе выбора оптовиков
|
||||
useEffect(() => {
|
||||
@ -251,7 +283,7 @@ export function CreateSupplyPage() {
|
||||
}
|
||||
|
||||
const updateProductQuantity = (productId: string, quantity: number) => {
|
||||
const product = mockProducts.find(p => p.id === productId)
|
||||
const product = wholesalerProducts.find((p: { id: string }) => p.id === productId)
|
||||
if (!product || !selectedWholesaler) return
|
||||
|
||||
setSelectedProducts(prev => {
|
||||
@ -320,9 +352,9 @@ export function CreateSupplyPage() {
|
||||
// Рендер товаров оптовика
|
||||
if (selectedWholesaler && selectedVariant === 'wholesaler') {
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-56">
|
||||
<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 className="flex items-center space-x-4">
|
||||
@ -337,7 +369,7 @@ export function CreateSupplyPage() {
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Товары оптовика</h1>
|
||||
<p className="text-white/60">{selectedWholesaler.name} • {mockProducts.length} товаров</p>
|
||||
<p className="text-white/60">{selectedWholesaler.name} • {wholesalerProducts.length} товаров</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
@ -496,12 +528,39 @@ export function CreateSupplyPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{mockProducts.map((product) => {
|
||||
{productsLoading ? (
|
||||
<div className="flex items-center justify-center p-8 col-span-full">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent mx-auto mb-4"></div>
|
||||
<p className="text-white/60">Загружаем товары...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : wholesalerProducts.length === 0 ? (
|
||||
<div className="flex items-center justify-center p-8 col-span-full">
|
||||
<div className="text-center">
|
||||
<Package className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">У этого оптовика нет товаров</p>
|
||||
<p className="text-white/40 text-sm mt-2">Выберите другого оптовика</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{wholesalerProducts.map((product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
category?: { name: string };
|
||||
brand?: string;
|
||||
color?: string;
|
||||
size?: string;
|
||||
mainImage?: string;
|
||||
images?: string[]
|
||||
}) => {
|
||||
const selectedQuantity = getSelectedQuantity(product.id)
|
||||
const discountedPrice = product.discount
|
||||
? product.price * (1 - product.discount / 100)
|
||||
: product.price
|
||||
const discountedPrice = product.price // Убираем discount так как его нет в схеме
|
||||
|
||||
return (
|
||||
<Card key={product.id} className="bg-white/10 backdrop-blur border-white/20 overflow-hidden group hover:bg-white/15 hover:border-white/30 transition-all duration-300 hover:scale-105 hover:shadow-2xl">
|
||||
@ -519,14 +578,7 @@ export function CreateSupplyPage() {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Discount badge */}
|
||||
{product.discount && (
|
||||
<div className="absolute top-2 left-2">
|
||||
<Badge className="bg-red-500 text-white border-0 font-bold text-xs">
|
||||
-{product.discount}%
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{/* Убираем discount badge так как поля нет в схеме */}
|
||||
|
||||
{/* Overlay с кнопками */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||
@ -550,16 +602,7 @@ export function CreateSupplyPage() {
|
||||
{product.brand}
|
||||
</Badge>
|
||||
)}
|
||||
{product.isNew && (
|
||||
<Badge className="bg-blue-500 text-white border-0 text-xs">
|
||||
NEW
|
||||
</Badge>
|
||||
)}
|
||||
{product.isBestseller && (
|
||||
<Badge className="bg-orange-500 text-white border-0 text-xs">
|
||||
ХИТ
|
||||
</Badge>
|
||||
)}
|
||||
{/* Убираем isNew и isBestseller так как этих полей нет в схеме */}
|
||||
</div>
|
||||
<h3 className="text-white font-semibold text-sm mb-1 line-clamp-2 leading-tight">
|
||||
{product.name}
|
||||
@ -578,11 +621,7 @@ export function CreateSupplyPage() {
|
||||
<div className="text-white font-bold text-lg">
|
||||
{formatCurrency(discountedPrice)}
|
||||
</div>
|
||||
{product.discount && (
|
||||
<div className="text-white/40 text-xs line-through">
|
||||
{formatCurrency(product.price)}
|
||||
</div>
|
||||
)}
|
||||
{/* Убираем отображение оригинальной цены так как discount нет */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -633,11 +672,6 @@ export function CreateSupplyPage() {
|
||||
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 border border-green-500/30 rounded p-2">
|
||||
<div className="text-green-300 text-xs font-medium text-center">
|
||||
{formatCurrency(discountedPrice * selectedQuantity)}
|
||||
{product.discount && (
|
||||
<div className="text-green-200 text-xs">
|
||||
экономия {formatCurrency((product.price - discountedPrice) * selectedQuantity)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -646,6 +680,7 @@ export function CreateSupplyPage() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating корзина */}
|
||||
{selectedProducts.length > 0 && (
|
||||
@ -669,9 +704,9 @@ export function CreateSupplyPage() {
|
||||
// Рендер выбора оптовиков
|
||||
if (selectedVariant === 'wholesaler') {
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-56">
|
||||
<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 className="flex items-center space-x-4">
|
||||
@ -875,12 +910,66 @@ export function CreateSupplyPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{mockWholesalers.map((wholesaler) => (
|
||||
{/* Поиск */}
|
||||
<div className="mb-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
|
||||
<Input
|
||||
placeholder="Поиск оптовиков по названию или ИНН..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 glass-input text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{counterpartiesLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent mx-auto mb-4"></div>
|
||||
<p className="text-white/60">Загружаем оптовиков...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : filteredWholesalers.length === 0 ? (
|
||||
<div className="text-center p-8">
|
||||
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">
|
||||
{searchQuery ? 'Оптовики не найдены' : 'У вас нет контрагентов-оптовиков'}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
{searchQuery ? 'Попробуйте изменить условия поиска' : 'Добавьте оптовиков в разделе "Партнеры"'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredWholesalers.map((wholesaler: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
inn?: string;
|
||||
address?: string;
|
||||
phones?: { value: string }[];
|
||||
emails?: { value: string }[]
|
||||
}) => (
|
||||
<Card
|
||||
key={wholesaler.id}
|
||||
className="bg-white/10 backdrop-blur border-white/20 p-6 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30 hover:scale-105"
|
||||
onClick={() => setSelectedWholesaler(wholesaler)}
|
||||
onClick={() => {
|
||||
// Адаптируем данные под существующий интерфейс
|
||||
const adaptedWholesaler = {
|
||||
id: wholesaler.id,
|
||||
inn: wholesaler.inn || '',
|
||||
name: wholesaler.name || 'Неизвестная организация',
|
||||
fullName: wholesaler.fullName || wholesaler.name || 'Неизвестная организация',
|
||||
address: wholesaler.address || 'Адрес не указан',
|
||||
phone: wholesaler.phones?.[0]?.value,
|
||||
email: wholesaler.emails?.[0]?.value,
|
||||
rating: 4.5, // Временное значение
|
||||
productCount: 0, // Временное значение
|
||||
specialization: ['Оптовая торговля'] // Временное значение
|
||||
}
|
||||
setSelectedWholesaler(adaptedWholesaler)
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
@ -889,56 +978,46 @@ export function CreateSupplyPage() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-semibold text-lg mb-1 truncate">
|
||||
{wholesaler.name}
|
||||
{wholesaler.name || 'Неизвестная организация'}
|
||||
</h3>
|
||||
<p className="text-white/60 text-xs mb-2 truncate">
|
||||
{wholesaler.fullName}
|
||||
{wholesaler.fullName || wholesaler.name}
|
||||
</p>
|
||||
<div className="flex items-center space-x-1 mb-2">
|
||||
{renderStars(wholesaler.rating)}
|
||||
<span className="text-white/60 text-sm ml-2">{wholesaler.rating}</span>
|
||||
</div>
|
||||
{wholesaler.inn && (
|
||||
<p className="text-white/40 text-xs">
|
||||
ИНН: {wholesaler.inn}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<MapPin className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-white/80 text-sm truncate">{wholesaler.address}</span>
|
||||
</div>
|
||||
{wholesaler.address && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<MapPin className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-white/80 text-sm truncate">{wholesaler.address}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{wholesaler.phone && (
|
||||
{wholesaler.phones?.[0]?.value && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Phone className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-white/80 text-sm">{wholesaler.phone}</span>
|
||||
<span className="text-white/80 text-sm">{wholesaler.phones[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{wholesaler.email && (
|
||||
{wholesaler.emails?.[0]?.value && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Mail className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-white/80 text-sm truncate">{wholesaler.email}</span>
|
||||
<span className="text-white/80 text-sm truncate">{wholesaler.emails[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Package className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-white/80 text-sm">{wholesaler.productCount} товаров</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-white/60 text-xs">Специализация:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{wholesaler.specialization.map((spec, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs"
|
||||
>
|
||||
{spec}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">
|
||||
Контрагент
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-white/10">
|
||||
@ -948,6 +1027,7 @@ export function CreateSupplyPage() {
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating корзина */}
|
||||
{selectedProducts.length > 0 && !showSummary && (
|
||||
@ -970,9 +1050,9 @@ export function CreateSupplyPage() {
|
||||
|
||||
// Главная страница выбора варианта
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-56">
|
||||
<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 className="flex items-center space-x-4">
|
||||
|
Reference in New Issue
Block a user