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:
Veronika Smirnova
2025-08-27 12:26:16 +03:00
parent a80dee9758
commit b405daa1be
3 changed files with 118 additions and 66 deletions

View File

@ -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 />

View File

@ -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>

View File

@ -3,6 +3,7 @@ import { LucideIcon } from 'lucide-react'
// Основные типы данных
export interface Supply {
id: string
productId?: string // ID продукта для фильтрации истории поставок
name: string
description: string
price: number