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 { toast } from 'sonner'
|
||||||
|
|
||||||
import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations'
|
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 {
|
import type {
|
||||||
SelectedGoodsItem,
|
SelectedGoodsItem,
|
||||||
@ -27,6 +29,9 @@ interface UseSupplyCartProps {
|
|||||||
|
|
||||||
export function useSupplyCart({ selectedSupplier, allCounterparties, productRecipes }: UseSupplyCartProps) {
|
export function useSupplyCart({ selectedSupplier, allCounterparties, productRecipes }: UseSupplyCartProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 🔧 FEATURE FLAG: Использовать V2 систему для товаров
|
||||||
|
const USE_V2_GOODS_SYSTEM = process.env.NEXT_PUBLIC_USE_V2_GOODS === 'true'
|
||||||
|
|
||||||
// Состояния корзины и настроек
|
// Состояния корзины и настроек
|
||||||
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsItem[]>([])
|
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsItem[]>([])
|
||||||
@ -39,8 +44,9 @@ export function useSupplyCart({ selectedSupplier, allCounterparties, productReci
|
|||||||
const [selectedFulfillment, setSelectedFulfillment] = useState<string>('')
|
const [selectedFulfillment, setSelectedFulfillment] = useState<string>('')
|
||||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false)
|
const [isCreatingSupply, setIsCreatingSupply] = useState(false)
|
||||||
|
|
||||||
// Мутация создания поставки
|
// Мутации создания поставки - V1 и V2
|
||||||
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER)
|
const [createSupplyOrderV1] = useMutation(CREATE_SUPPLY_ORDER)
|
||||||
|
const [createSupplyOrderV2] = useMutation(CREATE_SELLER_GOODS_SUPPLY)
|
||||||
|
|
||||||
// Получаем логистические компании
|
// Получаем логистические компании
|
||||||
const logisticsCompanies = useMemo(() => {
|
const logisticsCompanies = useMemo(() => {
|
||||||
@ -195,57 +201,93 @@ export function useSupplyCart({ selectedSupplier, allCounterparties, productReci
|
|||||||
setIsCreatingSupply(true)
|
setIsCreatingSupply(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const inputData = {
|
if (USE_V2_GOODS_SYSTEM) {
|
||||||
partnerId: selectedSupplier?.id || '',
|
// 🚀 V2 СИСТЕМА - Нормализованная рецептура
|
||||||
fulfillmentCenterId: selectedFulfillment,
|
console.log('🆕 Используем V2 систему для создания товарной поставки')
|
||||||
deliveryDate: new Date(deliveryDate).toISOString(), // Конвертируем в ISO string для DateTime
|
|
||||||
logisticsPartnerId: selectedLogistics === 'auto' ? null : selectedLogistics,
|
const v2InputData = adaptV1ToV2Format(
|
||||||
items: selectedGoods.map((item) => {
|
{
|
||||||
const recipe = productRecipes[item.id] || {
|
partnerId: selectedSupplier?.id || '',
|
||||||
productId: item.id,
|
fulfillmentCenterId: selectedFulfillment,
|
||||||
selectedServices: [],
|
deliveryDate: new Date(deliveryDate).toISOString(),
|
||||||
selectedFFConsumables: [],
|
logisticsPartnerId: selectedLogistics === 'auto' ? null : selectedLogistics,
|
||||||
selectedSellerConsumables: [],
|
notes: selectedGoods
|
||||||
}
|
.map((item) => item.specialRequirements)
|
||||||
return {
|
.filter(Boolean)
|
||||||
|
.join('; '),
|
||||||
|
},
|
||||||
|
selectedGoods.map((item) => ({
|
||||||
productId: item.id,
|
productId: item.id,
|
||||||
quantity: item.selectedQuantity,
|
quantity: item.selectedQuantity,
|
||||||
recipe: {
|
})),
|
||||||
services: recipe.selectedServices || [],
|
productRecipes,
|
||||||
fulfillmentConsumables: recipe.selectedFFConsumables || [],
|
)
|
||||||
sellerConsumables: recipe.selectedSellerConsumables || [],
|
|
||||||
marketplaceCardId: recipe.selectedWBCard || null,
|
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)
|
||||||
}),
|
|
||||||
notes: selectedGoods
|
const result = await createSupplyOrderV2({
|
||||||
.map((item) => item.specialRequirements)
|
variables: { input: v2InputData },
|
||||||
.filter(Boolean)
|
})
|
||||||
.join('; '),
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('❌ Ошибка создания поставки:', error)
|
console.error('❌ Ошибка создания поставки:', error)
|
||||||
toast.error('Ошибка при создании поставки')
|
if (error instanceof Error) {
|
||||||
|
toast.error(error.message)
|
||||||
|
} else {
|
||||||
|
toast.error('Ошибка при создании поставки')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsCreatingSupply(false)
|
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 { Sidebar } from '@/components/dashboard/sidebar'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { GET_PENDING_SUPPLIES_COUNT, GET_MY_SUPPLY_ORDERS } from '@/graphql/queries'
|
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 { useAuth } from '@/hooks/useAuth'
|
||||||
import { useRealtime } from '@/hooks/useRealtime'
|
import { useRealtime } from '@/hooks/useRealtime'
|
||||||
import { useSidebar } from '@/hooks/useSidebar'
|
import { useSidebar } from '@/hooks/useSidebar'
|
||||||
@ -44,18 +45,53 @@ export function SuppliesDashboard() {
|
|||||||
errorPolicy: 'ignore',
|
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, {
|
const { data: mySuppliesData, loading: mySuppliesLoading, refetch: refetchMySupplies } = useQuery(GET_MY_SUPPLY_ORDERS, {
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
errorPolicy: 'all',
|
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({
|
useRealtime({
|
||||||
onEvent: (evt) => {
|
onEvent: (evt) => {
|
||||||
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
||||||
refetchPending()
|
refetchPending()
|
||||||
refetchMySupplies() // Обновляем поставки селлера при изменениях
|
refetchMySupplies() // Обновляем V1 поставки селлера при изменениях
|
||||||
|
refetchMyV2Goods() // Обновляем V2 поставки селлера при изменениях
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -396,13 +432,31 @@ export function SuppliesDashboard() {
|
|||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{/* ✅ ЕДИНАЯ ЛОГИКА для табов "Карточки" и "Поставщики" согласно rules2.md 9.5.3 */}
|
{/* ✅ ЕДИНАЯ ЛОГИКА для табов "Карточки" и "Поставщики" согласно rules2.md 9.5.3 */}
|
||||||
{(activeThirdTab === 'cards' || activeThirdTab === 'suppliers') && (
|
{(activeThirdTab === 'cards' || activeThirdTab === 'suppliers') && (
|
||||||
<AllSuppliesTab
|
<div>
|
||||||
pendingSupplyOrders={pendingCount?.supplyOrders || 0}
|
{/* V2 система индикатор */}
|
||||||
goodsSupplies={(mySuppliesData?.mySupplyOrders || []).filter((supply: any) =>
|
{USE_V2_GOODS_SYSTEM && (
|
||||||
supply.consumableType !== 'SELLER_CONSUMABLES',
|
<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>
|
</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({
|
const fulfillmentCenter = await prisma.organization.findUnique({
|
||||||
where: { id: fulfillmentCenterId },
|
where: { id: fulfillmentCenterId },
|
||||||
include: {
|
include: {
|
||||||
counterpartiesAsCounterparty: {
|
counterpartyOf: {
|
||||||
where: { organizationId: user.organizationId! },
|
where: { organizationId: user.organizationId! },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -315,7 +315,7 @@ export const sellerGoodsMutations = {
|
|||||||
throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип')
|
throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fulfillmentCenter.counterpartiesAsCounterparty.length === 0) {
|
if (fulfillmentCenter.counterpartyOf.length === 0) {
|
||||||
throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром')
|
throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +323,7 @@ export const sellerGoodsMutations = {
|
|||||||
const supplier = await prisma.organization.findUnique({
|
const supplier = await prisma.organization.findUnique({
|
||||||
where: { id: supplierId },
|
where: { id: supplierId },
|
||||||
include: {
|
include: {
|
||||||
counterpartiesAsCounterparty: {
|
counterpartyOf: {
|
||||||
where: { organizationId: user.organizationId! },
|
where: { organizationId: user.organizationId! },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -333,7 +333,7 @@ export const sellerGoodsMutations = {
|
|||||||
throw new GraphQLError('Поставщик не найден или имеет неверный тип')
|
throw new GraphQLError('Поставщик не найден или имеет неверный тип')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supplier.counterpartiesAsCounterparty.length === 0) {
|
if (supplier.counterpartyOf.length === 0) {
|
||||||
throw new GraphQLError('Нет партнерских отношений с данным поставщиком')
|
throw new GraphQLError('Нет партнерских отношений с данным поставщиком')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,8 +345,14 @@ export const sellerGoodsMutations = {
|
|||||||
throw new GraphQLError('Должен быть хотя бы один основной товар')
|
throw new GraphQLError('Должен быть хотя бы один основной товар')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем все товары в рецептуре
|
// Проверяем только основные товары (MAIN_PRODUCT) в рецептуре
|
||||||
for (const item of recipeItems) {
|
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({
|
const product = await prisma.product.findUnique({
|
||||||
where: { id: item.productId },
|
where: { id: item.productId },
|
||||||
})
|
})
|
||||||
@ -359,19 +365,17 @@ export const sellerGoodsMutations = {
|
|||||||
throw new GraphQLError(`Товар ${product.name} не принадлежит выбранному поставщику`)
|
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) {
|
if (item.quantity > availableStock) {
|
||||||
throw new GraphQLError(
|
throw new GraphQLError(
|
||||||
`Недостаточно остатков товара "${product.name}". ` +
|
`Недостаточно остатков товара "${product.name}". ` +
|
||||||
`Доступно: ${availableStock} шт., запрашивается: ${item.quantity} шт.`,
|
`Доступно: ${availableStock} шт., запрашивается: ${item.quantity} шт.`,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
totalCost += product.price.toNumber() * item.quantity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalCost += product.price.toNumber() * item.quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ
|
// 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ
|
||||||
@ -390,8 +394,14 @@ export const sellerGoodsMutations = {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Создаем записи рецептуры
|
// Создаем записи рецептуры только для MAIN_PRODUCT
|
||||||
for (const item of recipeItems) {
|
for (const item of recipeItems) {
|
||||||
|
// В V2 временно создаем только основные товары
|
||||||
|
if (item.recipeType !== 'MAIN_PRODUCT') {
|
||||||
|
console.log(`⚠️ Пропускаем создание записи для ${item.recipeType} товара ${item.productId}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
await tx.goodsSupplyRecipeItem.create({
|
await tx.goodsSupplyRecipeItem.create({
|
||||||
data: {
|
data: {
|
||||||
supplyOrderId: newOrder.id,
|
supplyOrderId: newOrder.id,
|
||||||
@ -402,16 +412,14 @@ export const sellerGoodsMutations = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Резервируем основные товары у поставщика
|
// Резервируем основные товары у поставщика
|
||||||
if (item.recipeType === 'MAIN_PRODUCT') {
|
await tx.product.update({
|
||||||
await tx.product.update({
|
where: { id: item.productId },
|
||||||
where: { id: item.productId },
|
data: {
|
||||||
data: {
|
ordered: {
|
||||||
ordered: {
|
increment: item.quantity,
|
||||||
increment: item.quantity,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return newOrder
|
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