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'
|
'use client'
|
||||||
|
|
||||||
import { useQuery } from '@apollo/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 React, { useState, useMemo, useCallback } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||||
import { GET_MY_FULFILLMENT_SUPPLIES } from '@/graphql/queries'
|
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 { useSidebar } from '@/hooks/useSidebar'
|
||||||
|
|
||||||
// Новые компоненты
|
|
||||||
import { SuppliesGrid } from './supplies-grid'
|
import { SuppliesGrid } from './supplies-grid'
|
||||||
import { SuppliesHeader } from './supplies-header'
|
import { SuppliesHeader } from './supplies-header'
|
||||||
import { SuppliesList } from './supplies-list'
|
import { SuppliesList } from './supplies-list'
|
||||||
import { SuppliesStats } from './supplies-stats'
|
import { SuppliesStats } from './supplies-stats'
|
||||||
|
|
||||||
// Типы
|
|
||||||
import { Supply, FilterState, SortState, ViewMode, GroupBy, StatusConfig } from './types'
|
import { Supply, FilterState, SortState, ViewMode, GroupBy, StatusConfig } from './types'
|
||||||
|
|
||||||
// Статусы расходников с цветами
|
// Статусы расходников с цветами
|
||||||
const STATUS_CONFIG = {
|
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: {
|
available: {
|
||||||
label: 'Доступен',
|
label: 'Доступен',
|
||||||
color: 'bg-green-500/20 text-green-300',
|
color: 'bg-green-500/20 text-green-300',
|
||||||
@ -52,7 +61,7 @@ export function FulfillmentSuppliesPage() {
|
|||||||
const [groupBy, setGroupBy] = useState<GroupBy>('none')
|
const [groupBy, setGroupBy] = useState<GroupBy>('none')
|
||||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(new Set())
|
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
// Загрузка данных
|
// Загрузка данных складских остатков
|
||||||
const {
|
const {
|
||||||
data: suppliesData,
|
data: suppliesData,
|
||||||
loading,
|
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 => {
|
const getStatusConfig = useCallback((supply: Supply): StatusConfig => {
|
||||||
@ -75,58 +127,53 @@ export function FulfillmentSuppliesPage() {
|
|||||||
|
|
||||||
const getSupplyDeliveries = useCallback(
|
const getSupplyDeliveries = useCallback(
|
||||||
(supply: Supply): Supply[] => {
|
(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 consolidatedSupplies = useMemo(() => {
|
||||||
const grouped = supplies.reduce(
|
// Корректируем агрегированные данные на основе истории поставок для точности
|
||||||
(acc, supply) => {
|
return supplies.map((supply) => {
|
||||||
const key = supply.article // НОВОЕ: группировка по артикулу СФ
|
const correctedSupply = getAggregatedSupplyData(supply)
|
||||||
// СТАРОЕ - ОТКАТ: const key = `${supply.name}-${supply.category}`
|
return {
|
||||||
if (!acc[key]) {
|
...correctedSupply,
|
||||||
acc[key] = {
|
// Переопределяем статус на основе исправленных остатков
|
||||||
...supply,
|
status: correctedSupply.currentStock > 0 ? 'На складе' : 'Недоступен',
|
||||||
currentStock: 0,
|
}
|
||||||
quantity: 0, // Общее количество поставленного (= заказанному)
|
})
|
||||||
shippedQuantity: 0, // Общее отправленное количество
|
}, [supplies, getAggregatedSupplyData])
|
||||||
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])
|
|
||||||
|
|
||||||
// Фильтрация и сортировка
|
// Фильтрация и сортировка
|
||||||
const filteredAndSortedSupplies = useMemo(() => {
|
const filteredAndSortedSupplies = useMemo(() => {
|
||||||
@ -135,7 +182,11 @@ export function FulfillmentSuppliesPage() {
|
|||||||
supply.name.toLowerCase().includes(filters.search.toLowerCase()) ||
|
supply.name.toLowerCase().includes(filters.search.toLowerCase()) ||
|
||||||
supply.description.toLowerCase().includes(filters.search.toLowerCase())
|
supply.description.toLowerCase().includes(filters.search.toLowerCase())
|
||||||
const matchesCategory = !filters.category || supply.category === filters.category
|
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 === 'available' && supply.currentStock > 0) ||
|
||||||
(filters.status === 'unavailable' && supply.currentStock === 0)
|
(filters.status === 'unavailable' && supply.currentStock === 0)
|
||||||
const matchesSupplier =
|
const matchesSupplier =
|
||||||
@ -147,12 +198,12 @@ export function FulfillmentSuppliesPage() {
|
|||||||
|
|
||||||
// Сортировка
|
// Сортировка
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
let aValue: any = a[sort.field]
|
let aValue: string | number = a[sort.field]
|
||||||
let bValue: any = b[sort.field]
|
let bValue: string | number = b[sort.field]
|
||||||
|
|
||||||
if (typeof aValue === 'string') {
|
if (typeof aValue === 'string') {
|
||||||
aValue = aValue.toLowerCase()
|
aValue = aValue.toLowerCase()
|
||||||
bValue = bValue.toLowerCase()
|
bValue = (bValue as string).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sort.direction === 'asc') {
|
if (sort.direction === 'asc') {
|
||||||
@ -173,7 +224,7 @@ export function FulfillmentSuppliesPage() {
|
|||||||
(acc, supply) => {
|
(acc, supply) => {
|
||||||
let key: string
|
let key: string
|
||||||
if (groupBy === 'status') {
|
if (groupBy === 'status') {
|
||||||
key = supply.currentStock > 0 ? 'Доступен' : 'Недоступен'
|
key = supply.currentStock > 0 ? 'На складе' : 'Недоступен'
|
||||||
} else {
|
} else {
|
||||||
key = supply[groupBy] || 'Без категории'
|
key = supply[groupBy] || 'Без категории'
|
||||||
}
|
}
|
||||||
@ -235,7 +286,7 @@ export function FulfillmentSuppliesPage() {
|
|||||||
toast.success('Данные обновлены')
|
toast.success('Данные обновлены')
|
||||||
}, [refetch])
|
}, [refetch])
|
||||||
|
|
||||||
if (loading) {
|
if (loading || deliveriesLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex overflow-hidden">
|
<div className="h-screen flex overflow-hidden">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'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 React, { useMemo } from 'react'
|
||||||
|
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
@ -74,15 +74,15 @@ export function SuppliesStats({ supplies }: SuppliesStatsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Мало на складе */}
|
{/* Остаток */}
|
||||||
<Card className="glass-card p-4">
|
<Card className="glass-card p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-white/60 uppercase tracking-wider">Мало на складе</p>
|
<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-2xl font-bold text-blue-300 mt-1">{formatNumber(stats.totalStock)} шт</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 bg-yellow-500/20 rounded-lg">
|
<div className="p-2 bg-blue-500/20 rounded-lg">
|
||||||
<AlertTriangle className="h-5 w-5 text-yellow-300" />
|
<Package className="h-5 w-5 text-blue-300" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -3,6 +3,7 @@ import { LucideIcon } from 'lucide-react'
|
|||||||
// Основные типы данных
|
// Основные типы данных
|
||||||
export interface Supply {
|
export interface Supply {
|
||||||
id: string
|
id: string
|
||||||
|
productId?: string // ID продукта для фильтрации истории поставок
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
price: number
|
price: number
|
||||||
|
Reference in New Issue
Block a user