feat: продолжение миграции V2 системы поставок товаров
- Добавлен 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 <noreply@anthropic.com>
This commit is contained in:
@ -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<SelectedGoodsItem[]>([])
|
||||
@ -39,8 +44,9 @@ export function useSupplyCart({ selectedSupplier, allCounterparties, productReci
|
||||
const [selectedFulfillment, setSelectedFulfillment] = useState<string>('')
|
||||
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)
|
||||
}
|
||||
|
97
src/components/supplies/supplies-dashboard-v2.tsx
Normal file
97
src/components/supplies/supplies-dashboard-v2.tsx
Normal file
@ -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 <div>Загрузка...</div>
|
||||
if (error) return <div>Ошибка: {error.message}</div>
|
||||
|
||||
const supplies = selectedTab === 'goods' ? getGoodsSupplies() : getConsumablesSupplies()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{USE_V2_GOODS_SYSTEM && selectedTab === 'goods' && (
|
||||
<div className="mb-4 p-2 bg-blue-50 text-blue-700 rounded">
|
||||
🆕 Используется V2 система товарных поставок
|
||||
</div>
|
||||
)}
|
||||
|
||||
{supplies.length === 0 ? (
|
||||
<div>Нет поставок</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{supplies.map((supply: any) => (
|
||||
<div key={supply.id} className="border p-4 rounded">
|
||||
<h3>Поставка #{supply.id}</h3>
|
||||
<p>Статус: {supply.status}</p>
|
||||
<p>Дата: {new Date(supply.createdAt).toLocaleDateString()}</p>
|
||||
{USE_V2_GOODS_SYSTEM && supply.recipeItems && (
|
||||
<p>Товаров: {supply.recipeItems.filter((i: any) => i.recipeType === 'MAIN_PRODUCT').length}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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() {
|
||||
<div className="h-full">
|
||||
{/* ✅ ЕДИНАЯ ЛОГИКА для табов "Карточки" и "Поставщики" согласно rules2.md 9.5.3 */}
|
||||
{(activeThirdTab === 'cards' || activeThirdTab === 'suppliers') && (
|
||||
<AllSuppliesTab
|
||||
pendingSupplyOrders={pendingCount?.supplyOrders || 0}
|
||||
goodsSupplies={(mySuppliesData?.mySupplyOrders || []).filter((supply: any) =>
|
||||
supply.consumableType !== 'SELLER_CONSUMABLES',
|
||||
<div>
|
||||
{/* V2 система индикатор */}
|
||||
{USE_V2_GOODS_SYSTEM && (
|
||||
<div className="mb-4 p-3 bg-blue-500/20 border border-blue-400/30 text-blue-200 rounded-lg">
|
||||
🆕 Используется V2 система товарных поставок
|
||||
</div>
|
||||
)}
|
||||
loading={mySuppliesLoading}
|
||||
/>
|
||||
|
||||
<AllSuppliesTab
|
||||
pendingSupplyOrders={pendingCount?.supplyOrders || 0}
|
||||
goodsSupplies={USE_V2_GOODS_SYSTEM
|
||||
? (myV2GoodsData?.mySellerGoodsSupplies || []).map((v2Supply: any) => ({
|
||||
// Адаптируем 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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
191
src/graphql/mutations/seller-goods-v2.ts
Normal file
191
src/graphql/mutations/seller-goods-v2.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
`
|
@ -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
|
||||
|
111
src/lib/utils/recipe-transformer.ts
Normal file
111
src/lib/utils/recipe-transformer.ts
Normal file
@ -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<string, ProductRecipe>,
|
||||
): 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<string, ProductRecipe>,
|
||||
) {
|
||||
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)),
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user