refactor(supplies): extract custom hooks for supply creation logic
ЭТАП 1.2: Безопасное выделение бизнес-логики в хуки - Create useSupplierSelection.ts: управление выбором поставщиков и поиском - Create useProductCatalog.ts: загрузка и управление каталогом товаров - Create useSupplyCart.ts: логика корзины и создания поставки - Create useRecipeBuilder.ts: построение рецептур товаров (услуги + расходники) Каждый хук инкапсулирует отдельную область ответственности: - Состояние и действия изолированы - GraphQL запросы сгруппированы по функциональности - Бизнес-логика отделена от UI компонентов - Полная типизация с TypeScript No functional changes - pure logic extraction for better maintainability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* ХУКА ДЛЯ ЛОГИКИ КАТАЛОГА ТОВАРОВ
|
||||
*
|
||||
* Выделена из create-suppliers-supply-page.tsx
|
||||
* Управляет загрузкой и фильтрацией товаров поставщика
|
||||
*/
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { useState, useMemo } from 'react'
|
||||
|
||||
import { GET_ORGANIZATION_PRODUCTS } from '@/graphql/queries'
|
||||
|
||||
import type { GoodsSupplier, GoodsProduct } from '../types/supply-creation.types'
|
||||
|
||||
interface UseProductCatalogProps {
|
||||
selectedSupplier: GoodsSupplier | null
|
||||
}
|
||||
|
||||
export function useProductCatalog({ selectedSupplier }: UseProductCatalogProps) {
|
||||
// Состояния
|
||||
const [productSearchQuery] = useState('')
|
||||
const [allSelectedProducts, setAllSelectedProducts] = useState<Array<GoodsProduct & { selectedQuantity: number }>>([])
|
||||
const [productQuantities, setProductQuantities] = useState<Record<string, number>>({})
|
||||
|
||||
// Загружаем каталог товаров согласно rules2.md 13.3
|
||||
const {
|
||||
data: productsData,
|
||||
loading: productsLoading,
|
||||
error: productsError,
|
||||
} = useQuery(GET_ORGANIZATION_PRODUCTS, {
|
||||
variables: {
|
||||
organizationId: selectedSupplier?.id || '',
|
||||
search: productSearchQuery,
|
||||
category: '',
|
||||
type: 'PRODUCT', // КРИТИЧЕСКИ ВАЖНО: показываем только PRODUCT, не CONSUMABLE
|
||||
},
|
||||
skip: !selectedSupplier || !selectedSupplier.id,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'all',
|
||||
onError: (error) => {
|
||||
try {
|
||||
console.warn('🚨 GET_ORGANIZATION_PRODUCTS ERROR:', {
|
||||
errorMessage: error?.message || 'Unknown error',
|
||||
hasGraphQLErrors: !!error?.graphQLErrors?.length,
|
||||
hasNetworkError: !!error?.networkError,
|
||||
variables: {
|
||||
organizationId: selectedSupplier?.id || 'not_selected',
|
||||
search: productSearchQuery || 'empty',
|
||||
category: '',
|
||||
type: 'PRODUCT',
|
||||
},
|
||||
selectedSupplier: selectedSupplier
|
||||
? {
|
||||
id: selectedSupplier.id,
|
||||
name: selectedSupplier.name || selectedSupplier.fullName || 'Unknown',
|
||||
}
|
||||
: 'not_selected',
|
||||
})
|
||||
} catch (logError) {
|
||||
console.warn('❌ Error in error handler:', logError)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Получаем товары выбранного поставщика
|
||||
const products = useMemo(() => {
|
||||
return (productsData?.organizationProducts || []).filter((product: GoodsProduct) => {
|
||||
try {
|
||||
return product && product.id && product.name
|
||||
} catch (error) {
|
||||
console.warn('❌ Error filtering product:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}, [productsData])
|
||||
|
||||
// Функции для работы с количествами товаров
|
||||
const getProductQuantity = (productId: string): number => {
|
||||
return productQuantities[productId] || 0
|
||||
}
|
||||
|
||||
const setProductQuantity = (productId: string, quantity: number): void => {
|
||||
setProductQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: Math.max(0, quantity),
|
||||
}))
|
||||
}
|
||||
|
||||
// Добавление товара в выбранные с количеством
|
||||
const addProductToSelected = (product: GoodsProduct, quantity: number = 1) => {
|
||||
const productWithQuantity = {
|
||||
...product,
|
||||
selectedQuantity: quantity,
|
||||
}
|
||||
|
||||
setAllSelectedProducts((prev) => {
|
||||
const existingIndex = prev.findIndex((p) => p.id === product.id)
|
||||
if (existingIndex >= 0) {
|
||||
// Обновляем количество существующего товара
|
||||
const updated = [...prev]
|
||||
updated[existingIndex] = { ...updated[existingIndex], selectedQuantity: quantity }
|
||||
return updated
|
||||
} else {
|
||||
// Добавляем новый товар
|
||||
return [...prev, productWithQuantity]
|
||||
}
|
||||
})
|
||||
|
||||
// Синхронизируем с productQuantities
|
||||
setProductQuantity(product.id, quantity)
|
||||
}
|
||||
|
||||
// Удаление товара из выбранных
|
||||
const removeProductFromSelected = (productId: string) => {
|
||||
setAllSelectedProducts((prev) => prev.filter((p) => p.id !== productId))
|
||||
setProductQuantities((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[productId]
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
// Обновление количества выбранного товара
|
||||
const updateSelectedProductQuantity = (productId: string, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeProductFromSelected(productId)
|
||||
return
|
||||
}
|
||||
|
||||
setAllSelectedProducts((prev) => prev.map((p) => (p.id === productId ? { ...p, selectedQuantity: quantity } : p)))
|
||||
setProductQuantity(productId, quantity)
|
||||
}
|
||||
|
||||
return {
|
||||
// Состояние товаров
|
||||
products,
|
||||
allSelectedProducts,
|
||||
setAllSelectedProducts,
|
||||
productQuantities,
|
||||
setProductQuantities,
|
||||
|
||||
// Статусы загрузки
|
||||
loading: productsLoading,
|
||||
error: productsError,
|
||||
|
||||
// Функции управления товарами
|
||||
getProductQuantity,
|
||||
setProductQuantity,
|
||||
addProductToSelected,
|
||||
removeProductFromSelected,
|
||||
updateSelectedProductQuantity,
|
||||
}
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
/**
|
||||
* ХУКА ДЛЯ ЛОГИКИ ПОСТРОЕНИЯ РЕЦЕПТУР
|
||||
*
|
||||
* Выделена из create-suppliers-supply-page.tsx
|
||||
* Управляет рецептурами товаров (услуги ФФ, расходники, WB карточки)
|
||||
*/
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { useState, useMemo } from 'react'
|
||||
|
||||
import {
|
||||
GET_COUNTERPARTY_SERVICES,
|
||||
GET_COUNTERPARTY_SUPPLIES,
|
||||
GET_AVAILABLE_SUPPLIES_FOR_RECIPE,
|
||||
} from '@/graphql/queries'
|
||||
|
||||
import type {
|
||||
ProductRecipe,
|
||||
FulfillmentService,
|
||||
FulfillmentConsumable,
|
||||
SellerConsumable,
|
||||
RecipeCostCalculation,
|
||||
} from '../types/supply-creation.types'
|
||||
|
||||
interface UseRecipeBuilderProps {
|
||||
selectedFulfillment: string
|
||||
}
|
||||
|
||||
export function useRecipeBuilder({ selectedFulfillment }: UseRecipeBuilderProps) {
|
||||
// Состояние рецептур
|
||||
const [productRecipes, setProductRecipes] = useState<Record<string, ProductRecipe>>({})
|
||||
|
||||
// Загрузка услуг фулфилмента
|
||||
const { data: servicesData } = useQuery(GET_COUNTERPARTY_SERVICES, {
|
||||
variables: { counterpartyId: selectedFulfillment },
|
||||
skip: !selectedFulfillment,
|
||||
})
|
||||
|
||||
// Загрузка расходников фулфилмента
|
||||
const { data: ffConsumablesData } = useQuery(GET_COUNTERPARTY_SUPPLIES, {
|
||||
variables: {
|
||||
counterpartyId: selectedFulfillment,
|
||||
type: 'CONSUMABLE',
|
||||
},
|
||||
skip: !selectedFulfillment,
|
||||
})
|
||||
|
||||
// Загрузка расходников селлера
|
||||
const { data: sellerConsumablesData } = useQuery(GET_AVAILABLE_SUPPLIES_FOR_RECIPE, {
|
||||
variables: { type: 'CONSUMABLE' },
|
||||
})
|
||||
|
||||
// Обработка данных
|
||||
const fulfillmentServices: FulfillmentService[] = useMemo(() => {
|
||||
try {
|
||||
return servicesData?.counterpartyServices || []
|
||||
} catch (error) {
|
||||
console.warn('❌ Error processing fulfillment services:', error)
|
||||
return []
|
||||
}
|
||||
}, [servicesData])
|
||||
|
||||
const fulfillmentConsumables: FulfillmentConsumable[] = useMemo(() => {
|
||||
try {
|
||||
return ffConsumablesData?.counterpartySupplies || []
|
||||
} catch (error) {
|
||||
console.warn('❌ Error processing fulfillment consumables:', error)
|
||||
return []
|
||||
}
|
||||
}, [ffConsumablesData])
|
||||
|
||||
const sellerConsumables: SellerConsumable[] = useMemo(() => {
|
||||
try {
|
||||
return sellerConsumablesData?.availableSuppliesForRecipe || []
|
||||
} catch (error) {
|
||||
console.warn('❌ Error processing seller consumables:', error)
|
||||
return []
|
||||
}
|
||||
}, [sellerConsumablesData])
|
||||
|
||||
// Инициализация рецептуры для товара
|
||||
const initializeProductRecipe = (productId: string) => {
|
||||
if (!productRecipes[productId]) {
|
||||
setProductRecipes((prev) => ({
|
||||
...prev,
|
||||
[productId]: {
|
||||
productId,
|
||||
selectedServices: [],
|
||||
selectedFFConsumables: [],
|
||||
selectedSellerConsumables: [],
|
||||
selectedWBCard: undefined,
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Переключение услуги фулфилмента
|
||||
const toggleService = (productId: string, serviceId: string) => {
|
||||
setProductRecipes((prev) => {
|
||||
const recipe = prev[productId] || {
|
||||
productId,
|
||||
selectedServices: [],
|
||||
selectedFFConsumables: [],
|
||||
selectedSellerConsumables: [],
|
||||
}
|
||||
|
||||
const isSelected = recipe.selectedServices.includes(serviceId)
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[productId]: {
|
||||
...recipe,
|
||||
selectedServices: isSelected
|
||||
? recipe.selectedServices.filter((id) => id !== serviceId)
|
||||
: [...recipe.selectedServices, serviceId],
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Переключение расходников ФФ
|
||||
const toggleFFConsumable = (productId: string, consumableId: string) => {
|
||||
setProductRecipes((prev) => {
|
||||
const recipe = prev[productId] || {
|
||||
productId,
|
||||
selectedServices: [],
|
||||
selectedFFConsumables: [],
|
||||
selectedSellerConsumables: [],
|
||||
}
|
||||
|
||||
const isSelected = recipe.selectedFFConsumables.includes(consumableId)
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[productId]: {
|
||||
...recipe,
|
||||
selectedFFConsumables: isSelected
|
||||
? recipe.selectedFFConsumables.filter((id) => id !== consumableId)
|
||||
: [...recipe.selectedFFConsumables, consumableId],
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Переключение расходников селлера
|
||||
const toggleSellerConsumable = (productId: string, consumableId: string) => {
|
||||
setProductRecipes((prev) => {
|
||||
const recipe = prev[productId] || {
|
||||
productId,
|
||||
selectedServices: [],
|
||||
selectedFFConsumables: [],
|
||||
selectedSellerConsumables: [],
|
||||
}
|
||||
|
||||
const isSelected = recipe.selectedSellerConsumables.includes(consumableId)
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[productId]: {
|
||||
...recipe,
|
||||
selectedSellerConsumables: isSelected
|
||||
? recipe.selectedSellerConsumables.filter((id) => id !== consumableId)
|
||||
: [...recipe.selectedSellerConsumables, consumableId],
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Установка WB карточки
|
||||
const setWBCard = (productId: string, cardId: string) => {
|
||||
setProductRecipes((prev) => ({
|
||||
...prev,
|
||||
[productId]: {
|
||||
...prev[productId],
|
||||
selectedWBCard: cardId === 'none' ? undefined : cardId,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// Расчет стоимости рецептуры
|
||||
const calculateRecipeCost = (productId: string): RecipeCostCalculation => {
|
||||
const recipe = productRecipes[productId]
|
||||
if (!recipe) return { services: 0, consumables: 0, total: 0 }
|
||||
|
||||
// Стоимость услуг
|
||||
const servicesTotal = recipe.selectedServices.reduce((sum, serviceId) => {
|
||||
const service = fulfillmentServices.find((s) => s.id === serviceId)
|
||||
return sum + (service ? service.price : 0)
|
||||
}, 0)
|
||||
|
||||
// Стоимость расходников ФФ
|
||||
const ffConsumablesTotal = recipe.selectedFFConsumables.reduce((sum, consumableId) => {
|
||||
const consumable = fulfillmentConsumables.find((c) => c.id === consumableId)
|
||||
return sum + (consumable ? consumable.price : 0)
|
||||
}, 0)
|
||||
|
||||
// Стоимость расходников селлера
|
||||
const sellerConsumablesTotal = recipe.selectedSellerConsumables.reduce((sum, consumableId) => {
|
||||
const consumable = sellerConsumables.find((c) => c.id === consumableId)
|
||||
return sum + (consumable ? consumable.pricePerUnit : 0)
|
||||
}, 0)
|
||||
|
||||
const consumables = ffConsumablesTotal + sellerConsumablesTotal
|
||||
const total = servicesTotal + consumables
|
||||
|
||||
return { services: servicesTotal, consumables, total }
|
||||
}
|
||||
|
||||
// Получение рецептуры товара
|
||||
const getProductRecipe = (productId: string): ProductRecipe | undefined => {
|
||||
return productRecipes[productId]
|
||||
}
|
||||
|
||||
// Проверка наличия выбранных компонентов для товара
|
||||
const hasSelectedComponents = (productId: string): boolean => {
|
||||
const recipe = productRecipes[productId]
|
||||
if (!recipe) return false
|
||||
|
||||
return (
|
||||
recipe.selectedServices.length > 0 ||
|
||||
recipe.selectedFFConsumables.length > 0 ||
|
||||
recipe.selectedSellerConsumables.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
// Состояние рецептур
|
||||
productRecipes,
|
||||
setProductRecipes,
|
||||
|
||||
// Данные компонентов
|
||||
fulfillmentServices,
|
||||
fulfillmentConsumables,
|
||||
sellerConsumables,
|
||||
|
||||
// Функции управления рецептурами
|
||||
initializeProductRecipe,
|
||||
toggleService,
|
||||
toggleFFConsumable,
|
||||
toggleSellerConsumable,
|
||||
setWBCard,
|
||||
|
||||
// Функции расчетов и получения данных
|
||||
calculateRecipeCost,
|
||||
getProductRecipe,
|
||||
hasSelectedComponents,
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* ХУКА ДЛЯ ЛОГИКИ ВЫБОРА ПОСТАВЩИКОВ
|
||||
*
|
||||
* Выделена из create-suppliers-supply-page.tsx
|
||||
* Управляет состоянием выбранного поставщика и поиском
|
||||
*/
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { useState, useMemo } from 'react'
|
||||
|
||||
import { GET_MY_COUNTERPARTIES } from '@/graphql/queries'
|
||||
|
||||
import type { GoodsSupplier } from '../types/supply-creation.types'
|
||||
|
||||
export function useSupplierSelection() {
|
||||
// Состояния
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<GoodsSupplier | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// Загружаем партнеров-поставщиков согласно rules2.md 13.3
|
||||
const {
|
||||
data: counterpartiesData,
|
||||
loading: counterpartiesLoading,
|
||||
error: counterpartiesError,
|
||||
} = useQuery(GET_MY_COUNTERPARTIES, {
|
||||
errorPolicy: 'all', // Показываем все ошибки, но не прерываем работу
|
||||
onError: (error) => {
|
||||
try {
|
||||
console.warn('🚨 GET_MY_COUNTERPARTIES ERROR:', {
|
||||
errorMessage: error?.message || 'Unknown error',
|
||||
hasGraphQLErrors: !!error?.graphQLErrors?.length,
|
||||
hasNetworkError: !!error?.networkError,
|
||||
})
|
||||
} catch (logError) {
|
||||
console.warn('❌ Error in counterparties error handler:', logError)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Обработка данных контрагентов
|
||||
const allCounterparties: GoodsSupplier[] = useMemo(() => {
|
||||
try {
|
||||
return counterpartiesData?.myCounterparties || []
|
||||
} catch (error) {
|
||||
console.warn('❌ Error processing counterparties data:', error)
|
||||
return []
|
||||
}
|
||||
}, [counterpartiesData])
|
||||
|
||||
// Показываем только партнеров с типом WHOLESALE согласно rules2.md 13.3
|
||||
const wholesaleSuppliers = useMemo(() => {
|
||||
return allCounterparties.filter((cp: GoodsSupplier) => {
|
||||
try {
|
||||
return cp && cp.type === 'WHOLESALE'
|
||||
} catch (error) {
|
||||
console.warn('❌ Error filtering wholesale supplier:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}, [allCounterparties])
|
||||
|
||||
// Фильтрация поставщиков по поисковому запросу
|
||||
const suppliers = useMemo(() => {
|
||||
return wholesaleSuppliers.filter((cp: GoodsSupplier) => {
|
||||
try {
|
||||
if (!searchQuery.trim()) return true
|
||||
const searchLower = searchQuery.toLowerCase()
|
||||
const name = (cp.name || cp.fullName || '').toLowerCase()
|
||||
const inn = (cp.inn || '').toLowerCase()
|
||||
return name.includes(searchLower) || inn.includes(searchLower)
|
||||
} catch (error) {
|
||||
console.warn('❌ Error filtering supplier by search:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}, [wholesaleSuppliers, searchQuery])
|
||||
|
||||
// Утилитарные функции для работы с рынками
|
||||
const getMarketLabel = (market?: string) => {
|
||||
switch (market) {
|
||||
case 'wildberries':
|
||||
return 'WB'
|
||||
case 'ozon':
|
||||
return 'OZON'
|
||||
case 'yandexmarket':
|
||||
return 'YM'
|
||||
default:
|
||||
return 'Универсальный'
|
||||
}
|
||||
}
|
||||
|
||||
const getMarketBadgeStyle = (market?: string) => {
|
||||
switch (market) {
|
||||
case 'wildberries':
|
||||
return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
|
||||
case 'ozon':
|
||||
return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
|
||||
case 'yandexmarket':
|
||||
return 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
selectedSupplier,
|
||||
setSelectedSupplier,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
|
||||
// Данные
|
||||
suppliers,
|
||||
allCounterparties,
|
||||
wholesaleSuppliers,
|
||||
|
||||
// Статусы загрузки
|
||||
loading: counterpartiesLoading,
|
||||
error: counterpartiesError,
|
||||
|
||||
// Утилиты
|
||||
getMarketLabel,
|
||||
getMarketBadgeStyle,
|
||||
}
|
||||
}
|
227
src/components/supplies/create-suppliers/hooks/useSupplyCart.ts
Normal file
227
src/components/supplies/create-suppliers/hooks/useSupplyCart.ts
Normal file
@ -0,0 +1,227 @@
|
||||
/**
|
||||
* ХУКА ДЛЯ ЛОГИКИ КОРЗИНЫ ПОСТАВОК
|
||||
*
|
||||
* Выделена из create-suppliers-supply-page.tsx
|
||||
* Управляет корзиной товаров и настройками поставки
|
||||
*/
|
||||
|
||||
import { useMutation } from '@apollo/client'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations'
|
||||
|
||||
import type {
|
||||
SelectedGoodsItem,
|
||||
GoodsSupplier,
|
||||
GoodsProduct,
|
||||
ProductRecipe,
|
||||
SupplyCreationFormData,
|
||||
} from '../types/supply-creation.types'
|
||||
|
||||
interface UseSupplyCartProps {
|
||||
selectedSupplier: GoodsSupplier | null
|
||||
allCounterparties: GoodsSupplier[]
|
||||
productRecipes: Record<string, ProductRecipe>
|
||||
}
|
||||
|
||||
export function useSupplyCart({ selectedSupplier, allCounterparties, productRecipes }: UseSupplyCartProps) {
|
||||
const router = useRouter()
|
||||
|
||||
// Состояния корзины и настроек
|
||||
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsItem[]>([])
|
||||
const [deliveryDate, setDeliveryDate] = useState('')
|
||||
const [selectedLogistics, setSelectedLogistics] = useState<string>('auto')
|
||||
const [selectedFulfillment, setSelectedFulfillment] = useState<string>('')
|
||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false)
|
||||
|
||||
// Мутация создания поставки
|
||||
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER)
|
||||
|
||||
// Получаем логистические компании
|
||||
const logisticsCompanies = useMemo(() => {
|
||||
return allCounterparties?.filter((partner) => partner.type === 'LOGIST') || []
|
||||
}, [allCounterparties])
|
||||
|
||||
// Добавление товара в корзину
|
||||
const addToCart = (
|
||||
product: GoodsProduct,
|
||||
quantity: number,
|
||||
additionalData?: {
|
||||
completeness?: string
|
||||
recipe?: string
|
||||
specialRequirements?: string
|
||||
parameters?: Array<{ name: string; value: string }>
|
||||
},
|
||||
) => {
|
||||
if (!selectedSupplier) {
|
||||
toast.error('Сначала выберите поставщика')
|
||||
return
|
||||
}
|
||||
|
||||
if (quantity <= 0) {
|
||||
toast.error('Укажите количество товара')
|
||||
return
|
||||
}
|
||||
|
||||
const existingItemIndex = selectedGoods.findIndex((item) => item.id === product.id)
|
||||
|
||||
if (existingItemIndex >= 0) {
|
||||
// Обновляем существующий товар
|
||||
setSelectedGoods((prev) => {
|
||||
const updated = [...prev]
|
||||
updated[existingItemIndex] = {
|
||||
...updated[existingItemIndex],
|
||||
selectedQuantity: quantity,
|
||||
...additionalData,
|
||||
}
|
||||
return updated
|
||||
})
|
||||
toast.success('Количество товара обновлено')
|
||||
} else {
|
||||
// Добавляем новый товар
|
||||
const newItem: SelectedGoodsItem = {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
sku: product.article,
|
||||
price: product.price,
|
||||
selectedQuantity: quantity,
|
||||
unit: product.unit,
|
||||
category: product.category?.name,
|
||||
supplierId: selectedSupplier?.id || '',
|
||||
supplierName: selectedSupplier?.name || selectedSupplier?.fullName || '',
|
||||
completeness: additionalData?.completeness,
|
||||
recipe: additionalData?.recipe,
|
||||
specialRequirements: additionalData?.specialRequirements,
|
||||
parameters: additionalData?.parameters,
|
||||
}
|
||||
|
||||
setSelectedGoods((prev) => [...prev, newItem])
|
||||
toast.success('Товар добавлен в корзину')
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление товара из корзины
|
||||
const removeFromCart = (itemId: string) => {
|
||||
setSelectedGoods((prev) => prev.filter((item) => item.id !== itemId))
|
||||
toast.success('Товар удален из корзины')
|
||||
}
|
||||
|
||||
// Функция расчета полной стоимости товара с рецептурой
|
||||
const getProductTotalWithRecipe = (productId: string, quantity: number) => {
|
||||
const product = selectedGoods.find((p) => p.id === productId)
|
||||
if (!product) return 0
|
||||
|
||||
const baseTotal = product.price * quantity
|
||||
const recipe = productRecipes[productId]
|
||||
|
||||
if (!recipe) return baseTotal
|
||||
|
||||
// Здесь будет логика расчета стоимости услуг и расходников
|
||||
// Пока возвращаем базовую стоимость
|
||||
return baseTotal
|
||||
}
|
||||
|
||||
// Расчеты для корзины
|
||||
const totalGoodsAmount = useMemo(() => {
|
||||
return selectedGoods.reduce((sum, item) => {
|
||||
return sum + getProductTotalWithRecipe(item.id, item.selectedQuantity)
|
||||
}, 0)
|
||||
}, [selectedGoods, productRecipes])
|
||||
|
||||
const totalQuantity = useMemo(() => {
|
||||
return selectedGoods.reduce((sum, item) => sum + item.selectedQuantity, 0)
|
||||
}, [selectedGoods])
|
||||
|
||||
// Валидация формы
|
||||
const hasRequiredServices = useMemo(() => {
|
||||
return selectedGoods.every((item) => productRecipes[item.id]?.selectedServices?.length > 0)
|
||||
}, [selectedGoods, productRecipes])
|
||||
|
||||
const isFormValid = useMemo(() => {
|
||||
return selectedSupplier && selectedGoods.length > 0 && deliveryDate && selectedFulfillment && hasRequiredServices
|
||||
}, [selectedSupplier, selectedGoods.length, deliveryDate, selectedFulfillment, hasRequiredServices])
|
||||
|
||||
// Создание поставки
|
||||
const handleCreateSupply = async () => {
|
||||
if (!isFormValid) {
|
||||
if (!hasRequiredServices) {
|
||||
toast.error('Каждый товар должен иметь минимум 1 услугу фулфилмента')
|
||||
} else {
|
||||
toast.error('Заполните все обязательные поля')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedSupplier) {
|
||||
toast.error('Поставщик не выбран')
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreatingSupply(true)
|
||||
|
||||
try {
|
||||
await createSupplyOrder({
|
||||
variables: {
|
||||
supplierId: selectedSupplier?.id || '',
|
||||
fulfillmentCenterId: selectedFulfillment,
|
||||
items: selectedGoods.map((item) => ({
|
||||
productId: item.id,
|
||||
quantity: item.selectedQuantity,
|
||||
recipe: productRecipes[item.id] || {
|
||||
productId: item.id,
|
||||
selectedServices: [],
|
||||
selectedFFConsumables: [],
|
||||
selectedSellerConsumables: [],
|
||||
},
|
||||
})),
|
||||
deliveryDate: deliveryDate,
|
||||
logistics: selectedLogistics,
|
||||
specialRequirements: selectedGoods
|
||||
.map((item) => item.specialRequirements)
|
||||
.filter(Boolean)
|
||||
.join('; '),
|
||||
} satisfies SupplyCreationFormData,
|
||||
})
|
||||
|
||||
toast.success('Поставка успешно создана!')
|
||||
router.push('/supplies')
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка создания поставки:', error)
|
||||
toast.error('Ошибка при создании поставки')
|
||||
} finally {
|
||||
setIsCreatingSupply(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Состояние корзины
|
||||
selectedGoods,
|
||||
setSelectedGoods,
|
||||
deliveryDate,
|
||||
setDeliveryDate,
|
||||
selectedLogistics,
|
||||
setSelectedLogistics,
|
||||
selectedFulfillment,
|
||||
setSelectedFulfillment,
|
||||
isCreatingSupply,
|
||||
|
||||
// Данные
|
||||
logisticsCompanies,
|
||||
|
||||
// Расчеты
|
||||
totalGoodsAmount,
|
||||
totalQuantity,
|
||||
|
||||
// Валидация
|
||||
hasRequiredServices,
|
||||
isFormValid,
|
||||
|
||||
// Функции управления корзиной
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
getProductTotalWithRecipe,
|
||||
handleCreateSupply,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user