fix(fulfillment): исправить группировку поставок и синхронизацию статистики
- Исправить группировку в истории поставок - теперь 3 отдельные поставки показываются как 3 строки - Добавить фильтрацию по productId вместо name для корректной связи данных - Исправить отображение статусов поставок (toLowerCase для UPPERCASE статусов) - Заменить карточку "Мало на складе" на "Остаток" с общей суммой currentStock - Добавить функцию getAggregatedSupplyData для корректного подсчёта из истории - Синхронизировать fetchPolicy: 'cache-and-network' для связанных компонентов 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,25 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Package, Wrench, AlertTriangle, CheckCircle, Clock } from 'lucide-react'
|
||||
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',
|
||||
@ -52,7 +61,7 @@ export function FulfillmentSuppliesPage() {
|
||||
const [groupBy, setGroupBy] = useState<GroupBy>('none')
|
||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(new Set())
|
||||
|
||||
// Загрузка данных
|
||||
// Загрузка данных складских остатков
|
||||
const {
|
||||
data: suppliesData,
|
||||
loading,
|
||||
@ -65,8 +74,51 @@ export function FulfillmentSuppliesPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const supplies: Supply[] = suppliesData?.myFulfillmentSupplies || []
|
||||
// Загрузка истории поставок для детализации
|
||||
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 => {
|
||||
@ -75,58 +127,53 @@ export function FulfillmentSuppliesPage() {
|
||||
|
||||
const getSupplyDeliveries = useCallback(
|
||||
(supply: Supply): Supply[] => {
|
||||
return supplies.filter((s) => s.name === supply.name && s.category === supply.category)
|
||||
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[]
|
||||
},
|
||||
[supplies],
|
||||
[allDeliveries],
|
||||
)
|
||||
|
||||
// Объединение одинаковых расходников
|
||||
// V2 система уже возвращает агрегированные данные, дополнительная консолидация не нужна
|
||||
const consolidatedSupplies = useMemo(() => {
|
||||
const grouped = supplies.reduce(
|
||||
(acc, supply) => {
|
||||
const key = supply.article // НОВОЕ: группировка по артикулу СФ
|
||||
// СТАРОЕ - ОТКАТ: const key = `${supply.name}-${supply.category}`
|
||||
if (!acc[key]) {
|
||||
acc[key] = {
|
||||
...supply,
|
||||
currentStock: 0,
|
||||
quantity: 0, // Общее количество поставленного (= заказанному)
|
||||
shippedQuantity: 0, // Общее отправленное количество
|
||||
status: 'consolidated', // Не используем статус от отдельной поставки
|
||||
}
|
||||
}
|
||||
|
||||
// НОВОЕ: Учитываем принятые поставки (все варианты статусов)
|
||||
if (supply.status === 'доставлено' || supply.status === 'На складе' || supply.status === 'in-stock') {
|
||||
// СТАРОЕ - ОТКАТ: if (supply.status === 'in-stock') {
|
||||
// НОВОЕ: Используем actualQuantity (фактически поставленное) вместо quantity
|
||||
const actualQuantity = supply.actualQuantity ?? supply.quantity // По умолчанию = заказанному
|
||||
|
||||
acc[key].quantity += actualQuantity
|
||||
acc[key]!.shippedQuantity! += supply.shippedQuantity || 0
|
||||
acc[key]!.currentStock += actualQuantity - (supply.shippedQuantity || 0)
|
||||
|
||||
/* СТАРОЕ - ОТКАТ:
|
||||
// Суммируем только принятое количество
|
||||
acc[key].quantity += supply.quantity
|
||||
// Суммируем отправленное количество
|
||||
acc[key]!.shippedQuantity! += supply.shippedQuantity || 0
|
||||
// Остаток = Принятое - Отправленное
|
||||
acc[key]!.currentStock += supply.quantity - (supply.shippedQuantity || 0)
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Supply>,
|
||||
)
|
||||
|
||||
const result = Object.values(grouped)
|
||||
|
||||
|
||||
return result
|
||||
}, [supplies])
|
||||
// Корректируем агрегированные данные на основе истории поставок для точности
|
||||
return supplies.map((supply) => {
|
||||
const correctedSupply = getAggregatedSupplyData(supply)
|
||||
return {
|
||||
...correctedSupply,
|
||||
// Переопределяем статус на основе исправленных остатков
|
||||
status: correctedSupply.currentStock > 0 ? 'На складе' : 'Недоступен',
|
||||
}
|
||||
})
|
||||
}, [supplies, getAggregatedSupplyData])
|
||||
|
||||
// Фильтрация и сортировка
|
||||
const filteredAndSortedSupplies = useMemo(() => {
|
||||
@ -135,7 +182,11 @@ export function FulfillmentSuppliesPage() {
|
||||
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 ||
|
||||
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 =
|
||||
@ -147,12 +198,12 @@ export function FulfillmentSuppliesPage() {
|
||||
|
||||
// Сортировка
|
||||
filtered.sort((a, b) => {
|
||||
let aValue: any = a[sort.field]
|
||||
let bValue: any = b[sort.field]
|
||||
let aValue: string | number = a[sort.field]
|
||||
let bValue: string | number = b[sort.field]
|
||||
|
||||
if (typeof aValue === 'string') {
|
||||
aValue = aValue.toLowerCase()
|
||||
bValue = bValue.toLowerCase()
|
||||
bValue = (bValue as string).toLowerCase()
|
||||
}
|
||||
|
||||
if (sort.direction === 'asc') {
|
||||
@ -173,7 +224,7 @@ export function FulfillmentSuppliesPage() {
|
||||
(acc, supply) => {
|
||||
let key: string
|
||||
if (groupBy === 'status') {
|
||||
key = supply.currentStock > 0 ? 'Доступен' : 'Недоступен'
|
||||
key = supply.currentStock > 0 ? 'На складе' : 'Недоступен'
|
||||
} else {
|
||||
key = supply[groupBy] || 'Без категории'
|
||||
}
|
||||
@ -235,7 +286,7 @@ export function FulfillmentSuppliesPage() {
|
||||
toast.success('Данные обновлены')
|
||||
}, [refetch])
|
||||
|
||||
if (loading) {
|
||||
if (loading || deliveriesLoading) {
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Package, AlertTriangle, TrendingUp, TrendingDown, DollarSign, Activity } from 'lucide-react'
|
||||
import { Package, TrendingUp, TrendingDown, DollarSign, Activity } from 'lucide-react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { Card } from '@/components/ui/card'
|
||||
@ -74,15 +74,15 @@ export function SuppliesStats({ supplies }: SuppliesStatsProps) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Мало на складе */}
|
||||
{/* Остаток */}
|
||||
<Card className="glass-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-white/60 uppercase tracking-wider">Мало на складе</p>
|
||||
<p className="text-2xl font-bold text-yellow-300 mt-1">{formatNumber(stats.lowStock)}</p>
|
||||
<p className="text-xs font-medium text-white/60 uppercase tracking-wider">Остаток</p>
|
||||
<p className="text-2xl font-bold text-blue-300 mt-1">{formatNumber(stats.totalStock)} шт</p>
|
||||
</div>
|
||||
<div className="p-2 bg-yellow-500/20 rounded-lg">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-300" />
|
||||
<div className="p-2 bg-blue-500/20 rounded-lg">
|
||||
<Package className="h-5 w-5 text-blue-300" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -3,6 +3,7 @@ import { LucideIcon } from 'lucide-react'
|
||||
// Основные типы данных
|
||||
export interface Supply {
|
||||
id: string
|
||||
productId?: string // ID продукта для фильтрации истории поставок
|
||||
name: string
|
||||
description: string
|
||||
price: number
|
||||
|
Reference in New Issue
Block a user