'use client' import { useQuery } from '@apollo/client' import { AlertTriangle, CheckCircle } from 'lucide-react' import React, { useState, useMemo, useCallback } from 'react' import { toast } from 'sonner' import { Sidebar } from '@/components/dashboard/sidebar' import { GET_MY_FULFILLMENT_SUPPLIES } from '@/graphql/queries' import { GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES } from '@/graphql/queries/fulfillment-consumables-v2' import { useSidebar } from '@/hooks/useSidebar' import { SuppliesGrid } from './supplies-grid' import { SuppliesHeader } from './supplies-header' import { SuppliesList } from './supplies-list' import { SuppliesStats } from './supplies-stats' import { Supply, FilterState, SortState, ViewMode, GroupBy, StatusConfig } from './types' // Статусы расходников с цветами const STATUS_CONFIG = { 'На складе': { label: 'На складе', color: 'bg-green-500/20 text-green-300', icon: CheckCircle, }, Недоступен: { label: 'Недоступен', color: 'bg-red-500/20 text-red-300', icon: AlertTriangle, }, // Обратная совместимость available: { label: 'Доступен', color: 'bg-green-500/20 text-green-300', icon: CheckCircle, }, unavailable: { label: 'Недоступен', color: 'bg-red-500/20 text-red-300', icon: AlertTriangle, }, } as const export function FulfillmentSuppliesPage() { const { getSidebarMargin } = useSidebar() // Состояния const [viewMode, setViewMode] = useState('list') const [filters, setFilters] = useState({ search: '', category: '', status: '', supplier: '', lowStock: false, }) const [sort, setSort] = useState({ field: 'name', direction: 'asc', }) const [showFilters, setShowFilters] = useState(false) const [groupBy, setGroupBy] = useState('none') const [expandedSupplies, setExpandedSupplies] = useState>(new Set()) // Загрузка данных складских остатков const { data: suppliesData, loading, error, refetch, } = useQuery(GET_MY_FULFILLMENT_SUPPLIES, { fetchPolicy: 'cache-and-network', onError: (error) => { toast.error('Ошибка загрузки расходников фулфилмента: ' + error.message) }, }) // Загрузка истории поставок для детализации const { data: deliveriesData, loading: deliveriesLoading } = useQuery(GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES, { fetchPolicy: 'cache-and-network', onError: (error) => { console.warn('Ошибка загрузки истории поставок:', error.message) }, }) const supplies = useMemo(() => suppliesData?.myFulfillmentSupplies || [], [suppliesData]) const allDeliveries = useMemo(() => deliveriesData?.myFulfillmentConsumableSupplies || [], [deliveriesData]) // Функция для корректировки агрегированных данных на основе истории поставок const getAggregatedSupplyData = useCallback( (supply: Supply) => { if (!supply.productId || !allDeliveries.length) return supply // Получаем все поставки этого товара const productDeliveries = allDeliveries.filter((delivery) => delivery.items?.some((item) => item.productId === supply.productId), ) if (!productDeliveries.length) return supply // Вычисляем суммы из истории поставок let totalReceived = 0 let totalShipped = 0 productDeliveries.forEach((delivery) => { const item = delivery.items?.find((item) => item.productId === supply.productId) if (item) { totalReceived += item.receivedQuantity || 0 totalShipped += item.shippedQuantity || 0 } }) // Возвращаем supply с исправленными данными return { ...supply, quantity: totalReceived, // ПОСТАВЛЕНО = сумма всех receivedQuantity currentStock: Math.max(0, totalReceived - totalShipped), // ОСТАТОК = получено - отгружено shippedQuantity: totalShipped, // ОТПРАВЛЕНО = сумма всех shippedQuantity } }, [allDeliveries], ) // Функции const getStatusConfig = useCallback((supply: Supply): StatusConfig => { return supply.currentStock > 0 ? STATUS_CONFIG.available : STATUS_CONFIG.unavailable }, []) const getSupplyDeliveries = useCallback( (supply: Supply): Supply[] => { if (!supply.productId || !allDeliveries.length) return [] // Фильтруем поставки по productId товара return allDeliveries .filter((delivery) => delivery.items?.some((item) => item.productId === supply.productId)) .map((delivery) => { // Преобразуем поставку в формат Supply для DeliveryDetails const item = delivery.items?.find((item) => item.productId === supply.productId) if (!item) return null return { id: delivery.id, productId: supply.productId, name: supply.name, description: supply.description, price: item.unitPrice || 0, quantity: item.requestedQuantity || 0, unit: supply.unit, category: supply.category, status: delivery.status ? delivery.status.toLowerCase() : 'pending', date: delivery.createdAt || '', supplier: delivery.supplier?.name || supply.supplier, minStock: supply.minStock, currentStock: item.receivedQuantity || 0, imageUrl: supply.imageUrl, createdAt: delivery.createdAt || '', updatedAt: delivery.updatedAt || '', shippedQuantity: item.shippedQuantity || 0, } }) .filter(Boolean) as Supply[] }, [allDeliveries], ) // V2 система уже возвращает агрегированные данные, дополнительная консолидация не нужна const consolidatedSupplies = useMemo(() => { // Корректируем агрегированные данные на основе истории поставок для точности return supplies.map((supply) => { const correctedSupply = getAggregatedSupplyData(supply) return { ...correctedSupply, // Переопределяем статус на основе исправленных остатков status: correctedSupply.currentStock > 0 ? 'На складе' : 'Недоступен', } }) }, [supplies, getAggregatedSupplyData]) // Фильтрация и сортировка const filteredAndSortedSupplies = useMemo(() => { const filtered = consolidatedSupplies.filter((supply) => { const matchesSearch = supply.name.toLowerCase().includes(filters.search.toLowerCase()) || supply.description.toLowerCase().includes(filters.search.toLowerCase()) const matchesCategory = !filters.category || supply.category === filters.category const matchesStatus = !filters.status || (filters.status === 'На складе' && supply.currentStock > 0) || (filters.status === 'Недоступен' && supply.currentStock === 0) || // Обратная совместимость (filters.status === 'available' && supply.currentStock > 0) || (filters.status === 'unavailable' && supply.currentStock === 0) const matchesSupplier = !filters.supplier || supply.supplier.toLowerCase().includes(filters.supplier.toLowerCase()) const matchesLowStock = !filters.lowStock || (supply.currentStock <= supply.minStock && supply.currentStock > 0) return matchesSearch && matchesCategory && matchesStatus && matchesSupplier && matchesLowStock }) // Сортировка filtered.sort((a, b) => { let aValue: string | number = a[sort.field] let bValue: string | number = b[sort.field] if (typeof aValue === 'string') { aValue = aValue.toLowerCase() bValue = (bValue as string).toLowerCase() } if (sort.direction === 'asc') { return aValue > bValue ? 1 : -1 } else { return aValue < bValue ? 1 : -1 } }) return filtered }, [consolidatedSupplies, filters, sort]) // Группировка const groupedSupplies = useMemo(() => { if (groupBy === 'none') return { 'Все расходники': filteredAndSortedSupplies } return filteredAndSortedSupplies.reduce( (acc, supply) => { let key: string if (groupBy === 'status') { key = supply.currentStock > 0 ? 'На складе' : 'Недоступен' } else { key = supply[groupBy] || 'Без категории' } if (!acc[key]) acc[key] = [] acc[key].push(supply) return acc }, {} as Record, ) }, [filteredAndSortedSupplies, groupBy]) // Обработчики const handleSort = useCallback((field: SortState['field']) => { setSort((prev) => ({ field, direction: prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc', })) }, []) const toggleSupplyExpansion = useCallback((supplyId: string) => { setExpandedSupplies((prev) => { const newSet = new Set(prev) if (newSet.has(supplyId)) { newSet.delete(supplyId) } else { newSet.add(supplyId) } return newSet }) }, []) const handleExport = useCallback(() => { const csvData = filteredAndSortedSupplies.map((supply) => ({ Название: supply.name, Описание: supply.description, Категория: supply.category, Статус: getStatusConfig(supply).label, 'Текущий остаток': supply.currentStock, 'Минимальный остаток': supply.minStock, Единица: supply.unit, Цена: supply.price, Поставщик: supply.supplier, 'Дата создания': new Date(supply.createdAt).toLocaleDateString('ru-RU'), })) const csv = [Object.keys(csvData[0]).join(','), ...csvData.map((row) => Object.values(row).join(','))].join('\n') const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = `расходники_фулфилмента_${new Date().toISOString().split('T')[0]}.csv` link.click() toast.success('Данные экспортированы в CSV') }, [filteredAndSortedSupplies, getStatusConfig]) const handleRefresh = useCallback(() => { refetch() toast.success('Данные обновлены') }, [refetch]) if (loading || deliveriesLoading) { return (
Загрузка...
) } if (error) { return (
Ошибка загрузки: {error.message}
) } return (
{/* Заголовок и фильтры */} setShowFilters(!showFilters)} onExport={handleExport} onRefresh={handleRefresh} /> {/* Статистика */} {/* Основной контент */}
{groupBy === 'none' ? ( // Без группировки <> {viewMode === 'grid' && ( )} {viewMode === 'list' && ( )} {viewMode === 'analytics' && (
Аналитический режим будет добавлен позже
)} ) : ( // С группировкой
{Object.entries(groupedSupplies).map(([groupName, groupSupplies]) => (

{groupName} ({groupSupplies.length})

{viewMode === 'grid' && ( )} {viewMode === 'list' && ( )}
))}
)}
) }