From c344a177b5a3d5ccbd40a0776d5b3fe6ed4d0566 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Mon, 1 Sep 2025 19:21:13 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D1=80=D0=BE=D0=B4=D0=BE=D0=BB?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20V2=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D1=8B=20=D0=BF=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D0=BE=D0=BA=20?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен feature flag USE_V2_GOODS_SYSTEM для переключения между V1 и V2 - Создан трансформер рецептур для конвертации V1 → V2 формата - Интегрирована V2 мутация CREATE_SELLER_GOODS_SUPPLY в useSupplyCart - Добавлен V2 запрос GET_MY_SELLER_GOODS_SUPPLIES в supplies-dashboard - Исправлены связи counterpartyOf в goods-supply-v2 resolver - Временно отключена валидация для не-MAIN_PRODUCT товаров в V2 - Создан новый компонент supplies-dashboard-v2 (в разработке) Изменения являются частью поэтапной миграции с V1 на V2 систему поставок 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../create-suppliers/hooks/useSupplyCart.ts | 136 ++++++++----- .../supplies/supplies-dashboard-v2.tsx | 97 +++++++++ .../supplies/supplies-dashboard.tsx | 72 ++++++- src/graphql/mutations/seller-goods-v2.ts | 191 ++++++++++++++++++ src/graphql/resolvers/goods-supply-v2.ts | 60 +++--- src/lib/utils/recipe-transformer.ts | 111 ++++++++++ 6 files changed, 585 insertions(+), 82 deletions(-) create mode 100644 src/components/supplies/supplies-dashboard-v2.tsx create mode 100644 src/graphql/mutations/seller-goods-v2.ts create mode 100644 src/lib/utils/recipe-transformer.ts diff --git a/src/components/supplies/create-suppliers/hooks/useSupplyCart.ts b/src/components/supplies/create-suppliers/hooks/useSupplyCart.ts index 562915f..8eb3e0f 100644 --- a/src/components/supplies/create-suppliers/hooks/useSupplyCart.ts +++ b/src/components/supplies/create-suppliers/hooks/useSupplyCart.ts @@ -11,6 +11,8 @@ import { useState, useMemo, useCallback } from 'react' import { toast } from 'sonner' import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations' +import { CREATE_SELLER_GOODS_SUPPLY } from '@/graphql/mutations/seller-goods-v2' +import { adaptV1ToV2Format } from '@/lib/utils/recipe-transformer' import type { SelectedGoodsItem, @@ -27,6 +29,9 @@ interface UseSupplyCartProps { export function useSupplyCart({ selectedSupplier, allCounterparties, productRecipes }: UseSupplyCartProps) { const router = useRouter() + + // 🔧 FEATURE FLAG: Использовать V2 систему для товаров + const USE_V2_GOODS_SYSTEM = process.env.NEXT_PUBLIC_USE_V2_GOODS === 'true' // Состояния корзины и настроек const [selectedGoods, setSelectedGoods] = useState([]) @@ -39,8 +44,9 @@ export function useSupplyCart({ selectedSupplier, allCounterparties, productReci const [selectedFulfillment, setSelectedFulfillment] = useState('') const [isCreatingSupply, setIsCreatingSupply] = useState(false) - // Мутация создания поставки - const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER) + // Мутации создания поставки - V1 и V2 + const [createSupplyOrderV1] = useMutation(CREATE_SUPPLY_ORDER) + const [createSupplyOrderV2] = useMutation(CREATE_SELLER_GOODS_SUPPLY) // Получаем логистические компании const logisticsCompanies = useMemo(() => { @@ -195,57 +201,93 @@ export function useSupplyCart({ selectedSupplier, allCounterparties, productReci setIsCreatingSupply(true) try { - const inputData = { - partnerId: selectedSupplier?.id || '', - fulfillmentCenterId: selectedFulfillment, - deliveryDate: new Date(deliveryDate).toISOString(), // Конвертируем в ISO string для DateTime - logisticsPartnerId: selectedLogistics === 'auto' ? null : selectedLogistics, - items: selectedGoods.map((item) => { - const recipe = productRecipes[item.id] || { - productId: item.id, - selectedServices: [], - selectedFFConsumables: [], - selectedSellerConsumables: [], - } - return { + if (USE_V2_GOODS_SYSTEM) { + // 🚀 V2 СИСТЕМА - Нормализованная рецептура + console.log('🆕 Используем V2 систему для создания товарной поставки') + + const v2InputData = adaptV1ToV2Format( + { + partnerId: selectedSupplier?.id || '', + fulfillmentCenterId: selectedFulfillment, + deliveryDate: new Date(deliveryDate).toISOString(), + logisticsPartnerId: selectedLogistics === 'auto' ? null : selectedLogistics, + notes: selectedGoods + .map((item) => item.specialRequirements) + .filter(Boolean) + .join('; '), + }, + selectedGoods.map((item) => ({ productId: item.id, quantity: item.selectedQuantity, - recipe: { - services: recipe.selectedServices || [], - fulfillmentConsumables: recipe.selectedFFConsumables || [], - sellerConsumables: recipe.selectedSellerConsumables || [], - marketplaceCardId: recipe.selectedWBCard || null, - }, - } - }), - notes: selectedGoods - .map((item) => item.specialRequirements) - .filter(Boolean) - .join('; '), + })), + productRecipes, + ) + + console.log('🔄 V2 данные для отправки:', JSON.stringify(v2InputData, null, 2)) + console.log('🔄 V2 recipeItems структура:', JSON.stringify(v2InputData.recipeItems, null, 2)) + console.log('🔄 Mutation CREATE_SELLER_GOODS_SUPPLY:', CREATE_SELLER_GOODS_SUPPLY) + + const result = await createSupplyOrderV2({ + variables: { input: v2InputData }, + }) + + if (result.data?.createSellerGoodsSupply?.success) { + toast.success('Поставка успешно создана через V2 систему!') + router.push('/supplies') + } else { + throw new Error(result.data?.createSellerGoodsSupply?.message || 'Ошибка создания поставки') + } + + } else { + // 📦 V1 СИСТЕМА - Старый формат (для обратной совместимости) + console.log('📦 Используем V1 систему для создания товарной поставки') + + const inputData = { + partnerId: selectedSupplier?.id || '', + fulfillmentCenterId: selectedFulfillment, + deliveryDate: new Date(deliveryDate).toISOString(), + logisticsPartnerId: selectedLogistics === 'auto' ? null : selectedLogistics, + items: selectedGoods.map((item) => { + const recipe = productRecipes[item.id] || { + productId: item.id, + selectedServices: [], + selectedFFConsumables: [], + selectedSellerConsumables: [], + } + return { + productId: item.id, + quantity: item.selectedQuantity, + recipe: { + services: recipe.selectedServices || [], + fulfillmentConsumables: recipe.selectedFFConsumables || [], + sellerConsumables: recipe.selectedSellerConsumables || [], + marketplaceCardId: recipe.selectedWBCard || null, + }, + } + }), + notes: selectedGoods + .map((item) => item.specialRequirements) + .filter(Boolean) + .join('; '), + } + + console.warn('🔍 V1 данные для отправки:', JSON.stringify(inputData, null, 2)) + + await createSupplyOrderV1({ + variables: { input: inputData }, + }) + + toast.success('Поставка успешно создана!') + router.push('/supplies') } - - console.warn('🚀 Отправляем данные поставки:', { - inputData, - selectedSupplier: selectedSupplier?.id, - selectedFulfillment, - selectedLogistics, - selectedGoodsCount: selectedGoods.length, - deliveryDateType: typeof deliveryDate, - deliveryDateValue: deliveryDate, - convertedDate: new Date(deliveryDate).toISOString(), - }) - console.warn('🔍 ДЕТАЛЬНАЯ ПРОВЕРКА inputData перед отправкой:', JSON.stringify(inputData, null, 2)) - - await createSupplyOrder({ - variables: { input: inputData }, - }) - - toast.success('Поставка успешно создана!') - router.push('/supplies') } catch (error) { console.error('❌ Ошибка создания поставки:', error) - toast.error('Ошибка при создании поставки') + if (error instanceof Error) { + toast.error(error.message) + } else { + toast.error('Ошибка при создании поставки') + } } finally { setIsCreatingSupply(false) } diff --git a/src/components/supplies/supplies-dashboard-v2.tsx b/src/components/supplies/supplies-dashboard-v2.tsx new file mode 100644 index 0000000..99b79d1 --- /dev/null +++ b/src/components/supplies/supplies-dashboard-v2.tsx @@ -0,0 +1,97 @@ +/** + * 🔄 V2 КОМПОНЕНТ ДЛЯ ОТОБРАЖЕНИЯ ПОСТАВОК + * Поддерживает как V1, так и V2 системы через feature flag + */ + +'use client' + +import { useQuery } from '@apollo/client' +import { useState } from 'react' + +import { GET_MY_SELLER_GOODS_SUPPLIES } from '@/graphql/mutations/seller-goods-v2' +import { GET_MY_SUPPLY_ORDERS } from '@/graphql/queries' + +interface SuppliesDashboardV2Props { + selectedTab: string +} + +export function SuppliesDashboardV2({ selectedTab }: SuppliesDashboardV2Props) { + const USE_V2_GOODS_SYSTEM = process.env.NEXT_PUBLIC_USE_V2_GOODS === 'true' + + // V1 запрос - для обратной совместимости + const { + data: v1Data, + loading: v1Loading, + error: v1Error, + } = useQuery(GET_MY_SUPPLY_ORDERS, { + skip: USE_V2_GOODS_SYSTEM && selectedTab === 'goods', + }) + + // V2 запрос - для товарных поставок + const { + data: v2Data, + loading: v2Loading, + error: v2Error, + } = useQuery(GET_MY_SELLER_GOODS_SUPPLIES, { + skip: !USE_V2_GOODS_SYSTEM || selectedTab !== 'goods', + }) + + // Объединяем данные в зависимости от системы + const getGoodsSupplies = () => { + if (selectedTab !== 'goods') return [] + + if (USE_V2_GOODS_SYSTEM) { + // V2 система - прямые данные + return v2Data?.mySellerGoodsSupplies || [] + } else { + // V1 система - фильтруем по типу + return (v1Data?.mySupplyOrders || []).filter( + (supply: any) => supply.consumableType !== 'SELLER_CONSUMABLES', + ) + } + } + + const getConsumablesSupplies = () => { + if (selectedTab !== 'consumables') return [] + + // Расходники пока остаются в V1 + return (v1Data?.mySupplyOrders || []).filter( + (supply: any) => supply.consumableType === 'SELLER_CONSUMABLES', + ) + } + + const loading = v1Loading || v2Loading + const error = v1Error || v2Error + + if (loading) return
Загрузка...
+ if (error) return
Ошибка: {error.message}
+ + const supplies = selectedTab === 'goods' ? getGoodsSupplies() : getConsumablesSupplies() + + return ( +
+ {USE_V2_GOODS_SYSTEM && selectedTab === 'goods' && ( +
+ 🆕 Используется V2 система товарных поставок +
+ )} + + {supplies.length === 0 ? ( +
Нет поставок
+ ) : ( +
+ {supplies.map((supply: any) => ( +
+

Поставка #{supply.id}

+

Статус: {supply.status}

+

Дата: {new Date(supply.createdAt).toLocaleDateString()}

+ {USE_V2_GOODS_SYSTEM && supply.recipeItems && ( +

Товаров: {supply.recipeItems.filter((i: any) => i.recipeType === 'MAIN_PRODUCT').length}

+ )} +
+ ))} +
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/supplies-dashboard.tsx b/src/components/supplies/supplies-dashboard.tsx index a484602..d8ec439 100644 --- a/src/components/supplies/supplies-dashboard.tsx +++ b/src/components/supplies/supplies-dashboard.tsx @@ -8,6 +8,7 @@ import React, { useState, useEffect } from 'react' import { Sidebar } from '@/components/dashboard/sidebar' import { Alert, AlertDescription } from '@/components/ui/alert' import { GET_PENDING_SUPPLIES_COUNT, GET_MY_SUPPLY_ORDERS } from '@/graphql/queries' +import { GET_MY_SELLER_GOODS_SUPPLIES } from '@/graphql/mutations/seller-goods-v2' import { useAuth } from '@/hooks/useAuth' import { useRealtime } from '@/hooks/useRealtime' import { useSidebar } from '@/hooks/useSidebar' @@ -44,18 +45,53 @@ export function SuppliesDashboard() { errorPolicy: 'ignore', }) - // Загружаем поставки селлера для многоуровневой таблицы + // 🔧 FEATURE FLAG: Используем V2 систему для товаров + const USE_V2_GOODS_SYSTEM = process.env.NEXT_PUBLIC_USE_V2_GOODS === 'true' + + // Загружаем поставки селлера для многоуровневой таблицы (V1) const { data: mySuppliesData, loading: mySuppliesLoading, refetch: refetchMySupplies } = useQuery(GET_MY_SUPPLY_ORDERS, { fetchPolicy: 'cache-and-network', errorPolicy: 'all', - skip: !user || user.organization?.type !== 'SELLER', // Загружаем только для селлеров + skip: !user || user.organization?.type !== 'SELLER' || (USE_V2_GOODS_SYSTEM && (activeSubTab === 'goods')), // Пропускаем V1 для товаров в V2 }) + // Загружаем V2 товарные поставки селлера + const { data: myV2GoodsData, loading: myV2GoodsLoading, refetch: refetchMyV2Goods, error: myV2GoodsError } = useQuery(GET_MY_SELLER_GOODS_SUPPLIES, { + fetchPolicy: 'cache-and-network', + errorPolicy: 'all', + skip: !user || user.organization?.type !== 'SELLER' || !USE_V2_GOODS_SYSTEM || activeSubTab !== 'goods', // Загружаем только для товаров в V2 + }) + + // Отладка V2 данных + console.log('🔍 V2 Query Skip Conditions:', { + USE_V2_GOODS_SYSTEM, + activeSubTab, + userType: user?.organization?.type, + hasUser: !!user, + shouldSkip: !user || user.organization?.type !== 'SELLER' || !USE_V2_GOODS_SYSTEM || activeSubTab !== 'goods', + }) + + if (USE_V2_GOODS_SYSTEM && activeSubTab === 'goods') { + console.log('🔍 V2 Query Debug:', { + loading: myV2GoodsLoading, + error: myV2GoodsError, + data: myV2GoodsData, + supplies: myV2GoodsData?.mySellerGoodsSupplies, + suppliesCount: myV2GoodsData?.mySellerGoodsSupplies?.length || 0, + }) + + // Детальная структура первой поставки + if (myV2GoodsData?.mySellerGoodsSupplies?.length > 0) { + console.log('🔍 V2 First Supply Structure:', JSON.stringify(myV2GoodsData.mySellerGoodsSupplies[0], null, 2)) + } + } + useRealtime({ onEvent: (evt) => { if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') { refetchPending() - refetchMySupplies() // Обновляем поставки селлера при изменениях + refetchMySupplies() // Обновляем V1 поставки селлера при изменениях + refetchMyV2Goods() // Обновляем V2 поставки селлера при изменениях } }, }) @@ -396,13 +432,31 @@ export function SuppliesDashboard() {
{/* ✅ ЕДИНАЯ ЛОГИКА для табов "Карточки" и "Поставщики" согласно rules2.md 9.5.3 */} {(activeThirdTab === 'cards' || activeThirdTab === 'suppliers') && ( - - supply.consumableType !== 'SELLER_CONSUMABLES', +
+ {/* V2 система индикатор */} + {USE_V2_GOODS_SYSTEM && ( +
+ 🆕 Используется V2 система товарных поставок +
)} - loading={mySuppliesLoading} - /> + + ({ + // Адаптируем V2 структуру под V1 формат для таблицы + ...v2Supply, + partner: v2Supply.supplier, // supplier → partner для совместимости + deliveryDate: v2Supply.requestedDeliveryDate, // для совместимости + items: v2Supply.recipeItems, // recipeItems → items для совместимости + })) + : (mySuppliesData?.mySupplyOrders || []).filter((supply: any) => + supply.consumableType !== 'SELLER_CONSUMABLES', + ) + } + loading={USE_V2_GOODS_SYSTEM ? myV2GoodsLoading : mySuppliesLoading} + /> +
)}
)} diff --git a/src/graphql/mutations/seller-goods-v2.ts b/src/graphql/mutations/seller-goods-v2.ts new file mode 100644 index 0000000..622f3da --- /dev/null +++ b/src/graphql/mutations/seller-goods-v2.ts @@ -0,0 +1,191 @@ +import { gql } from '@apollo/client' + +// ============================================================================= +// 📦 МУТАЦИИ ДЛЯ ТОВАРНЫХ ПОСТАВОК СЕЛЛЕРА V2 +// ============================================================================= + +export const CREATE_SELLER_GOODS_SUPPLY = gql` + mutation CreateSellerGoodsSupply($input: CreateSellerGoodsSupplyInput!) { + createSellerGoodsSupply(input: $input) { + success + message + supplyOrder { + id + status + createdAt + updatedAt + + # Основные данные + sellerId + seller { + id + name + inn + } + + fulfillmentCenterId + fulfillmentCenter { + id + name + address + } + + supplierId + supplier { + id + name + inn + } + + logisticsPartnerId + logisticsPartner { + id + name + } + + # Даты и статусы + requestedDeliveryDate + estimatedDeliveryDate + deliveredAt + shippedAt + + # Финансы + totalCostWithDelivery + actualDeliveryCost + + # Рецептура (нормализованная) + recipeItems { + id + productId + quantity + recipeType + product { + id + name + article + price + } + } + + # Метаданные + notes + supplierNotes + receiptNotes + trackingNumber + packagesCount + estimatedVolume + } + } + } +` + +export const UPDATE_SELLER_GOODS_SUPPLY_STATUS = gql` + mutation UpdateSellerGoodsSupplyStatus($id: ID!, $status: SellerSupplyOrderStatus!, $notes: String) { + updateSellerGoodsSupplyStatus(id: $id, status: $status, notes: $notes) { + id + status + updatedAt + supplierApprovedAt + shippedAt + deliveredAt + supplierNotes + receiptNotes + } + } +` + +export const CANCEL_SELLER_GOODS_SUPPLY = gql` + mutation CancelSellerGoodsSupply($id: ID!) { + cancelSellerGoodsSupply(id: $id) { + id + status + updatedAt + } + } +` + +// ============================================================================= +// 📊 ЗАПРОСЫ ДЛЯ ТОВАРНЫХ ПОСТАВОК V2 +// ============================================================================= + +export const GET_MY_SELLER_GOODS_SUPPLIES = gql` + query GetMySellerGoodsSupplies { + mySellerGoodsSupplies { + id + status + createdAt + updatedAt + + seller { + id + name + } + + fulfillmentCenter { + id + name + } + + supplier { + id + name + } + + requestedDeliveryDate + deliveredAt + + totalCostWithDelivery + + recipeItems { + id + productId + quantity + recipeType + product { + id + name + article + price + } + } + } + } +` + +export const GET_SELLER_GOODS_INVENTORY = gql` + query GetSellerGoodsInventory { + mySellerGoodsInventory { + id + + seller { + id + name + } + + fulfillmentCenter { + id + name + } + + product { + id + name + article + } + + currentStock + reservedStock + inPreparationStock + totalReceived + totalShipped + + averageCost + salePrice + + lastSupplyDate + lastShipDate + + notes + } + } +` \ No newline at end of file diff --git a/src/graphql/resolvers/goods-supply-v2.ts b/src/graphql/resolvers/goods-supply-v2.ts index 5066ad0..2839c1a 100644 --- a/src/graphql/resolvers/goods-supply-v2.ts +++ b/src/graphql/resolvers/goods-supply-v2.ts @@ -305,7 +305,7 @@ export const sellerGoodsMutations = { const fulfillmentCenter = await prisma.organization.findUnique({ where: { id: fulfillmentCenterId }, include: { - counterpartiesAsCounterparty: { + counterpartyOf: { where: { organizationId: user.organizationId! }, }, }, @@ -315,7 +315,7 @@ export const sellerGoodsMutations = { throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип') } - if (fulfillmentCenter.counterpartiesAsCounterparty.length === 0) { + if (fulfillmentCenter.counterpartyOf.length === 0) { throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром') } @@ -323,7 +323,7 @@ export const sellerGoodsMutations = { const supplier = await prisma.organization.findUnique({ where: { id: supplierId }, include: { - counterpartiesAsCounterparty: { + counterpartyOf: { where: { organizationId: user.organizationId! }, }, }, @@ -333,7 +333,7 @@ export const sellerGoodsMutations = { throw new GraphQLError('Поставщик не найден или имеет неверный тип') } - if (supplier.counterpartiesAsCounterparty.length === 0) { + if (supplier.counterpartyOf.length === 0) { throw new GraphQLError('Нет партнерских отношений с данным поставщиком') } @@ -345,8 +345,14 @@ export const sellerGoodsMutations = { throw new GraphQLError('Должен быть хотя бы один основной товар') } - // Проверяем все товары в рецептуре + // Проверяем только основные товары (MAIN_PRODUCT) в рецептуре for (const item of recipeItems) { + // В V2 временно валидируем только основные товары + if (item.recipeType !== 'MAIN_PRODUCT') { + console.log(`⚠️ Пропускаем валидацию ${item.recipeType} товара ${item.productId} - не поддерживается в V2`) + continue + } + const product = await prisma.product.findUnique({ where: { id: item.productId }, }) @@ -359,19 +365,17 @@ export const sellerGoodsMutations = { throw new GraphQLError(`Товар ${product.name} не принадлежит выбранному поставщику`) } - // Для основных товаров проверяем остатки - if (item.recipeType === 'MAIN_PRODUCT') { - const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0) + // Проверяем остатки основных товаров + const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0) - if (item.quantity > availableStock) { - throw new GraphQLError( - `Недостаточно остатков товара "${product.name}". ` + - `Доступно: ${availableStock} шт., запрашивается: ${item.quantity} шт.`, - ) - } - - totalCost += product.price.toNumber() * item.quantity + if (item.quantity > availableStock) { + throw new GraphQLError( + `Недостаточно остатков товара "${product.name}". ` + + `Доступно: ${availableStock} шт., запрашивается: ${item.quantity} шт.`, + ) } + + totalCost += product.price.toNumber() * item.quantity } // 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ @@ -390,8 +394,14 @@ export const sellerGoodsMutations = { }, }) - // Создаем записи рецептуры + // Создаем записи рецептуры только для MAIN_PRODUCT for (const item of recipeItems) { + // В V2 временно создаем только основные товары + if (item.recipeType !== 'MAIN_PRODUCT') { + console.log(`⚠️ Пропускаем создание записи для ${item.recipeType} товара ${item.productId}`) + continue + } + await tx.goodsSupplyRecipeItem.create({ data: { supplyOrderId: newOrder.id, @@ -402,16 +412,14 @@ export const sellerGoodsMutations = { }) // Резервируем основные товары у поставщика - if (item.recipeType === 'MAIN_PRODUCT') { - await tx.product.update({ - where: { id: item.productId }, - data: { - ordered: { - increment: item.quantity, - }, + await tx.product.update({ + where: { id: item.productId }, + data: { + ordered: { + increment: item.quantity, }, - }) - } + }, + }) } return newOrder diff --git a/src/lib/utils/recipe-transformer.ts b/src/lib/utils/recipe-transformer.ts new file mode 100644 index 0000000..a02d725 --- /dev/null +++ b/src/lib/utils/recipe-transformer.ts @@ -0,0 +1,111 @@ +// ============================================================================= +// 🔄 УТИЛИТЫ ДЛЯ ТРАНСФОРМАЦИИ РЕЦЕПТУРЫ V1 → V2 +// ============================================================================= + +import { RecipeType } from '@prisma/client' + +export interface V1RecipeItem { + productId: string + quantity: number + recipe: { + services: string[] + fulfillmentConsumables: string[] + sellerConsumables: string[] + marketplaceCardId: string | null + } +} + +export interface V2RecipeItem { + productId: string + quantity: number + recipeType: RecipeType +} + +export interface ProductRecipe { + productId: string + selectedServices: string[] + selectedFFConsumables: string[] + selectedSellerConsumables: string[] + selectedWBCard?: string | null +} + +/** + * Трансформирует рецептуру из V1 формата (вложенная) в V2 формат (нормализованная) + * @param items - Массив товаров с V1 рецептурой + * @param productRecipes - Объект с рецептурами для каждого товара + * @returns Массив нормализованных элементов рецептуры V2 + */ +export function transformRecipeToV2( + items: Array<{ productId: string; quantity: number }>, + productRecipes: Record, +): V2RecipeItem[] { + const recipeItems: V2RecipeItem[] = [] + + items.forEach(item => { + // 1. Основной товар всегда добавляется как MAIN_PRODUCT + recipeItems.push({ + productId: item.productId, + quantity: item.quantity, + recipeType: 'MAIN_PRODUCT' as RecipeType, + }) + + // 2. Получаем рецептуру для товара + const recipe = productRecipes[item.productId] + if (!recipe) return + + // 3. TODO: Услуги и расходники - временно отключены в V2 + // В V2 системе услуги и расходники должны быть записями в таблице Product + // Пока что создаем только основные товары + + // 4. TODO: Добавить поддержку услуг когда будут созданы записи Product для Services + // if (recipe.selectedServices?.length > 0) { ... } + + // 5. TODO: Добавить поддержку расходников когда будут созданы записи Product для Supplies + // if (recipe.selectedFFConsumables?.length > 0) { ... } + + // 6. Карточка маркетплейса пока не поддерживается в V2 + // TODO: Добавить поддержку marketplaceCardId когда будет готова модель + }) + + return recipeItems +} + +/** + * Адаптирует данные из V1 формата формы в V2 формат для мутации + * @param v1Data - Данные в формате V1 + * @param productRecipes - Рецептуры товаров + * @returns Данные в формате V2 для CreateSellerGoodsSupplyInput + */ +export function adaptV1ToV2Format( + v1Data: { + partnerId: string + fulfillmentCenterId: string + deliveryDate: string + logisticsPartnerId: string | null + notes: string + }, + items: Array<{ productId: string; quantity: number }>, + productRecipes: Record, +) { + return { + supplierId: v1Data.partnerId, // partnerId → supplierId + fulfillmentCenterId: v1Data.fulfillmentCenterId, + requestedDeliveryDate: v1Data.deliveryDate, // deliveryDate → requestedDeliveryDate + logisticsPartnerId: v1Data.logisticsPartnerId, + notes: v1Data.notes, + recipeItems: transformRecipeToV2(items, productRecipes), + } +} + +/** + * Группирует нормализованные элементы рецептуры обратно для отображения + * @param recipeItems - Нормализованные элементы V2 + * @returns Сгруппированные по типу элементы + */ +export function groupRecipeItemsByType(recipeItems: V2RecipeItem[]) { + return { + mainProducts: recipeItems.filter(item => item.recipeType === ('MAIN_PRODUCT' as RecipeType)), + services: recipeItems.filter(item => item.recipeType === ('COMPONENT' as RecipeType)), + consumables: recipeItems.filter(item => item.recipeType === ('ACCESSORY' as RecipeType) || item.recipeType === ('PACKAGING' as RecipeType)), + } +} \ No newline at end of file