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:
Veronika Smirnova
2025-08-12 19:52:44 +03:00
parent a198293861
commit 9988e55126
4 changed files with 753 additions and 0 deletions

View 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,
}
}