feat(fulfillment-supplies): миграция формы создания поставок расходников на v2 систему

- Обновлена форма создания поставок расходников фулфилмента для использования v2 GraphQL API
- Заменена мутация CREATE_SUPPLY_ORDER на CREATE_FULFILLMENT_CONSUMABLE_SUPPLY
- Обновлена структура input данных под новый формат v2
- Сделано поле логистики опциональным
- Добавлено поле notes для комментариев к поставке
- Обновлены refetchQueries на новые v2 запросы
- Исправлены TypeScript ошибки в интерфейсах
- Удалена дублирующая страница consumables-v2
- Сохранен оригинальный богатый UI интерфейс формы (819 строк)
- Подтверждена работа с новой таблицей FulfillmentConsumableSupplyOrder

Технические изменения:
- src/components/fulfillment-supplies/create-fulfillment-consumables-supply-v2.tsx - основная форма
- src/components/fulfillment-supplies/fulfillment-supplies-layout.tsx - обновлена навигация
- Добавлены недостающие поля quantity и ordered в интерфейсы продуктов
- Исправлены импорты и зависимости

Результат: форма полностью интегрирована с v2 системой поставок, которая использует отдельные таблицы для каждого типа поставок согласно новой архитектуре.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-25 07:52:46 +03:00
parent d05f0a6a93
commit 0e3ffc179c
34 changed files with 5795 additions and 565 deletions

View File

@ -0,0 +1,252 @@
import { gql } from '@apollo/client'
export const GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES = gql`
query GetMyFulfillmentConsumableSupplies {
myFulfillmentConsumableSupplies {
id
status
fulfillmentCenterId
fulfillmentCenter {
id
name
inn
}
requestedDeliveryDate
resalePricePerUnit
minStockLevel
notes
supplierId
supplier {
id
name
inn
}
supplierApprovedAt
packagesCount
estimatedVolume
supplierContractId
supplierNotes
logisticsPartnerId
logisticsPartner {
id
name
inn
}
estimatedDeliveryDate
routeId
logisticsCost
logisticsNotes
shippedAt
trackingNumber
receivedAt
receivedById
receivedBy {
id
managerName
phone
}
actualQuantity
defectQuantity
receiptNotes
items {
id
productId
product {
id
name
article
price
quantity
mainImage
}
requestedQuantity
approvedQuantity
shippedQuantity
receivedQuantity
defectQuantity
unitPrice
totalPrice
}
createdAt
updatedAt
}
}
`
export const GET_FULFILLMENT_CONSUMABLE_SUPPLY = gql`
query GetFulfillmentConsumableSupply($id: ID!) {
fulfillmentConsumableSupply(id: $id) {
id
status
fulfillmentCenterId
fulfillmentCenter {
id
name
inn
}
requestedDeliveryDate
resalePricePerUnit
minStockLevel
notes
supplierId
supplier {
id
name
inn
}
supplierApprovedAt
packagesCount
estimatedVolume
supplierContractId
supplierNotes
logisticsPartnerId
logisticsPartner {
id
name
inn
}
estimatedDeliveryDate
routeId
logisticsCost
logisticsNotes
shippedAt
trackingNumber
receivedAt
receivedById
receivedBy {
id
managerName
phone
}
actualQuantity
defectQuantity
receiptNotes
items {
id
productId
product {
id
name
article
price
quantity
mainImage
}
requestedQuantity
approvedQuantity
shippedQuantity
receivedQuantity
defectQuantity
unitPrice
totalPrice
}
createdAt
updatedAt
}
}
`
export const GET_MY_SUPPLIER_CONSUMABLE_SUPPLIES = gql`
query GetMySupplierConsumableSupplies {
mySupplierConsumableSupplies {
id
status
fulfillmentCenterId
fulfillmentCenter {
id
name
inn
}
requestedDeliveryDate
resalePricePerUnit
minStockLevel
notes
supplierId
supplier {
id
name
inn
}
supplierApprovedAt
packagesCount
estimatedVolume
supplierContractId
supplierNotes
logisticsPartnerId
logisticsPartner {
id
name
inn
}
estimatedDeliveryDate
routeId
logisticsCost
logisticsNotes
shippedAt
trackingNumber
receivedAt
receivedById
receivedBy {
id
managerName
phone
}
actualQuantity
defectQuantity
receiptNotes
items {
id
productId
product {
id
name
article
price
quantity
mainImage
}
requestedQuantity
approvedQuantity
shippedQuantity
receivedQuantity
defectQuantity
unitPrice
totalPrice
}
createdAt
updatedAt
}
}
`
export const CREATE_FULFILLMENT_CONSUMABLE_SUPPLY = gql`
mutation CreateFulfillmentConsumableSupply($input: CreateFulfillmentConsumableSupplyInput!) {
createFulfillmentConsumableSupply(input: $input) {
success
message
supplyOrder {
id
status
createdAt
}
}
}
`

View File

@ -12,6 +12,9 @@ import { WildberriesService } from '@/services/wildberries-service'
import '@/lib/seed-init' // Автоматическая инициализация БД
// Импорт новых resolvers для системы поставок v2
import { fulfillmentConsumableV2Queries, fulfillmentConsumableV2Mutations } from './resolvers/fulfillment-consumables-v2'
// 🔒 СИСТЕМА БЕЗОПАСНОСТИ - импорты
import { CommercialDataAudit } from './security/commercial-data-audit'
import { createSecurityContext } from './security/index'
@ -2762,6 +2765,7 @@ export const resolvers = {
return {
...item,
price: item.price || 0, // Исправлено: защита от null значения в существующих данных
recipe,
}
}),
@ -2792,6 +2796,9 @@ export const resolvers = {
throw new GraphQLError(`Ошибка получения поставок: ${error instanceof Error ? error.message : String(error)}`)
}
},
// Новая система поставок v2
...fulfillmentConsumableV2Queries,
},
Mutation: {
@ -5148,7 +5155,7 @@ export const resolvers = {
return {
productId: item.productId,
quantity: item.quantity,
price: product.price,
price: product.price || 0, // Исправлено: защита от null значения
totalPrice: new Prisma.Decimal(itemTotal),
// Извлечение данных рецептуры из объекта recipe
services: item.recipe?.services || [],
@ -10230,6 +10237,10 @@ resolvers.Mutation = {
}
}
},
// Добавляем v2 mutations через spread
...fulfillmentConsumableV2Mutations
}
/* // Резолвер для парсинга JSON рецептуры в SupplyOrderItem
SupplyOrderItem: {
@ -10248,6 +10259,9 @@ resolvers.Mutation = {
},
},
*/
}
// ===============================================
// НОВАЯ СИСТЕМА ПОСТАВОК V2.0 - RESOLVERS
// ===============================================
export default resolvers

View File

@ -0,0 +1,268 @@
import { GraphQLError } from 'graphql'
import { Context } from '../context'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
export const fulfillmentConsumableV2Queries = {
myFulfillmentConsumableSupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент-центров')
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching fulfillment consumable supplies:', error)
return [] // Возвращаем пустой массив вместо throw
}
},
fulfillmentConsumableSupply: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверка доступа
if (
user.organization.type === 'FULFILLMENT' &&
supply.fulfillmentCenterId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (
user.organization.type === 'WHOLESALE' &&
supply.supplierId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
return supply
} catch (error) {
console.error('Error fetching fulfillment consumable supply:', error)
throw new GraphQLError('Ошибка получения поставки')
}
},
// Заявки на поставки для поставщиков (новая система v2)
mySupplierConsumableSupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'WHOLESALE') {
return []
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
supplierId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching supplier consumable supplies:', error)
return []
}
},
}
export const fulfillmentConsumableV2Mutations = {
createFulfillmentConsumableSupply: async (
_: unknown,
args: {
input: {
supplierId: string
requestedDeliveryDate: string
items: Array<{
productId: string
requestedQuantity: number
}>
notes?: string
}
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент-центры могут создавать поставки расходников')
}
// Проверяем что поставщик существует и является WHOLESALE
const supplier = await prisma.organization.findUnique({
where: { id: args.input.supplierId },
})
if (!supplier || supplier.type !== 'WHOLESALE') {
throw new GraphQLError('Поставщик не найден или не является оптовиком')
}
// Проверяем что все товары существуют и принадлежат поставщику
const productIds = args.input.items.map(item => item.productId)
const products = await prisma.product.findMany({
where: {
id: { in: productIds },
organizationId: supplier.id,
type: 'CONSUMABLE',
},
})
if (products.length !== productIds.length) {
throw new GraphQLError('Некоторые товары не найдены или не принадлежат поставщику')
}
// Создаем поставку с items
const supplyOrder = await prisma.fulfillmentConsumableSupplyOrder.create({
data: {
fulfillmentCenterId: user.organizationId!,
supplierId: supplier.id,
requestedDeliveryDate: new Date(args.input.requestedDeliveryDate),
notes: args.input.notes,
items: {
create: args.input.items.map(item => {
const product = products.find(p => p.id === item.productId)!
return {
productId: item.productId,
requestedQuantity: item.requestedQuantity,
unitPrice: product.price,
totalPrice: product.price.mul(item.requestedQuantity),
}
}),
},
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Отправляем уведомление поставщику о новой заявке
await notifyOrganization(supplier.id, {
type: 'supply-order:new',
title: 'Новая заявка на поставку расходников',
message: `Фулфилмент-центр "${user.organization.name}" создал заявку на поставку расходников`,
data: {
supplyOrderId: supplyOrder.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
fulfillmentCenterName: user.organization.name,
itemsCount: args.input.items.length,
requestedDeliveryDate: args.input.requestedDeliveryDate,
},
})
return {
success: true,
message: 'Поставка расходников создана успешно',
supplyOrder,
}
} catch (error) {
console.error('Error creating fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка создания поставки',
supplyOrder: null,
}
}
},
}

View File

@ -8,6 +8,7 @@ import { referralResolvers } from './referrals'
import { integrateSecurityWithExistingResolvers } from './secure-integration'
import { secureSuppliesResolvers } from './secure-supplies'
import { suppliesResolvers } from './supplies'
import { fulfillmentConsumableV2Queries, fulfillmentConsumableV2Mutations } from './fulfillment-consumables-v2'
// Типы для резолверов
interface ResolverObject {
@ -104,6 +105,12 @@ const mergedResolvers = mergeResolvers(
// БЕЗОПАСНЫЕ резолверы поставок
secureSuppliesResolvers,
// НОВЫЕ резолверы для системы поставок v2
{
Query: fulfillmentConsumableV2Queries,
Mutation: fulfillmentConsumableV2Mutations,
},
)
// Применяем middleware безопасности ко всем резолверам

View File

@ -1655,4 +1655,114 @@ export const typeDefs = gql`
REFERRAL
AUTO_BUSINESS
}
# ===============================================
# НОВАЯ СИСТЕМА ПОСТАВОК V2.0
# ===============================================
# Новый enum для статусов поставок v2
enum SupplyOrderStatusV2 {
PENDING # Ожидает одобрения поставщика
SUPPLIER_APPROVED # Одобрено поставщиком
LOGISTICS_CONFIRMED # Логистика подтверждена
SHIPPED # Отгружено поставщиком
IN_TRANSIT # В пути
DELIVERED # Доставлено и принято
CANCELLED # Отменено
}
# Типы для поставок расходников фулфилмента
type FulfillmentConsumableSupplyOrder {
id: ID!
status: SupplyOrderStatusV2!
fulfillmentCenterId: ID!
fulfillmentCenter: Organization!
requestedDeliveryDate: DateTime!
resalePricePerUnit: Float
minStockLevel: Int
notes: String
# Данные поставщика
supplierId: ID
supplier: Organization
supplierApprovedAt: DateTime
packagesCount: Int
estimatedVolume: Float
supplierContractId: String
supplierNotes: String
# Данные логистики
logisticsPartnerId: ID
logisticsPartner: Organization
estimatedDeliveryDate: DateTime
routeId: ID
logisticsCost: Float
logisticsNotes: String
# Данные отгрузки
shippedAt: DateTime
trackingNumber: String
# Данные приемки
receivedAt: DateTime
receivedById: ID
receivedBy: User
actualQuantity: Int
defectQuantity: Int
receiptNotes: String
items: [FulfillmentConsumableSupplyItem!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type FulfillmentConsumableSupplyItem {
id: ID!
productId: ID!
product: Product!
requestedQuantity: Int!
approvedQuantity: Int
shippedQuantity: Int
receivedQuantity: Int
defectQuantity: Int
unitPrice: Float!
totalPrice: Float!
createdAt: DateTime!
updatedAt: DateTime!
}
# Input типы для создания поставок
input CreateFulfillmentConsumableSupplyInput {
supplierId: ID!
requestedDeliveryDate: DateTime!
items: [FulfillmentConsumableSupplyItemInput!]!
notes: String
}
input FulfillmentConsumableSupplyItemInput {
productId: ID!
requestedQuantity: Int!
}
# Response типы
type CreateFulfillmentConsumableSupplyResult {
success: Boolean!
message: String!
supplyOrder: FulfillmentConsumableSupplyOrder
}
# Расширяем Query и Mutation для новой системы
extend type Query {
# Новые запросы для системы поставок v2
myFulfillmentConsumableSupplies: [FulfillmentConsumableSupplyOrder!]!
mySupplierConsumableSupplies: [FulfillmentConsumableSupplyOrder!]!
fulfillmentConsumableSupply(id: ID!): FulfillmentConsumableSupplyOrder
}
extend type Mutation {
# Новые мутации для системы поставок v2
createFulfillmentConsumableSupply(
input: CreateFulfillmentConsumableSupplyInput!
): CreateFulfillmentConsumableSupplyResult!
}
`