Files
sfera/src/components/fulfillment-supplies/materials-supplies/materials-supplies-tab.tsx
Veronika Smirnova ead17fd56c feat: implement comprehensive supplies management system
- Create new supplies dashboard with multi-level navigation structure
- Add fulfillment supplies section with goods/materials/returns tabs
- Add marketplace supplies section with Wildberries/Ozon tabs
- Update existing supplies pages to use new dashboard
- Fix height alignment in statistics blocks
- Remove redundant title from materials supplies section
2025-07-21 13:25:16 +03:00

344 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { 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 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>
<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(getTotalAmount())}</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">{getTotalQuantity()}</p>
</div>
</div>
</Card>
<Card className="glass-card p-3 h-[60px]">
<div className="flex items-center space-x-2 h-full">
<div className="p-1.5 bg-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>
{/* Кнопка заказа */}
<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">
<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>
)
}