Обновления системы после анализа и оптимизации архитектуры
- Обновлена схема Prisma с новыми полями и связями - Актуализированы правила системы в rules-complete.md - Оптимизированы GraphQL типы, запросы и мутации - Улучшены компоненты интерфейса и валидация данных - Исправлены критические ESLint ошибки: удалены неиспользуемые импорты и переменные - Добавлены тестовые файлы для проверки функционала 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -687,28 +687,10 @@ export const resolvers = {
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
}
|
||||
|
||||
// Получаем заказы поставок, где фулфилмент является получателем,
|
||||
// но НЕ создателем (т.е. селлеры заказали расходники для фулфилмента)
|
||||
const sellerSupplyOrders = await prisma.supplyOrder.findMany({
|
||||
where: {
|
||||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||||
organizationId: { not: currentUser.organization.id }, // Создатель - НЕ мы
|
||||
status: 'DELIVERED', // Только доставленные
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
partner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// Проверяем, что это фулфилмент центр
|
||||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||||
return [] // Только фулфилменты имеют расходники
|
||||
}
|
||||
|
||||
// Получаем ВСЕ расходники из таблицы supply для фулфилмента
|
||||
const allSupplies = await prisma.supply.findMany({
|
||||
@ -717,52 +699,39 @@ export const resolvers = {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
// Получаем все заказы фулфилмента для себя (чтобы исключить их расходники)
|
||||
const fulfillmentOwnOrders = await prisma.supplyOrder.findMany({
|
||||
where: {
|
||||
organizationId: currentUser.organization.id, // Созданы фулфилментом
|
||||
fulfillmentCenterId: currentUser.organization.id, // Для себя
|
||||
status: 'DELIVERED',
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// Преобразуем старую структуру в новую согласно GraphQL схеме
|
||||
const transformedSupplies = allSupplies.map((supply) => ({
|
||||
id: supply.id,
|
||||
name: supply.name,
|
||||
description: supply.description,
|
||||
pricePerUnit: supply.price ? parseFloat(supply.price.toString()) : null, // Конвертируем Decimal в Number
|
||||
unit: supply.unit || 'шт', // Единица измерения
|
||||
imageUrl: supply.imageUrl,
|
||||
warehouseStock: supply.currentStock || 0, // Остаток на складе
|
||||
isAvailable: (supply.currentStock || 0) > 0, // Есть ли в наличии
|
||||
warehouseConsumableId: supply.id, // Связь со складом (пока используем тот же ID)
|
||||
createdAt: supply.createdAt,
|
||||
updatedAt: supply.updatedAt,
|
||||
organization: supply.organization,
|
||||
}))
|
||||
|
||||
// Создаем набор названий товаров из заказов фулфилмента для себя
|
||||
const fulfillmentProductNames = new Set(
|
||||
fulfillmentOwnOrders.flatMap((order) => order.items.map((item) => item.product.name)),
|
||||
)
|
||||
|
||||
// Фильтруем расходники: исключаем те, что созданы заказами фулфилмента для себя
|
||||
const sellerSupplies = allSupplies.filter((supply) => {
|
||||
// Если расходник соответствует товару из заказа фулфилмента для себя,
|
||||
// то это расходник фулфилмента, а не селлера
|
||||
return !fulfillmentProductNames.has(supply.name)
|
||||
})
|
||||
|
||||
// Логирование для отладки
|
||||
console.warn('🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥')
|
||||
console.warn('📊 Расходники селлеров:', {
|
||||
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
|
||||
organizationId: currentUser.organization.id,
|
||||
organizationType: currentUser.organization.type,
|
||||
allSuppliesCount: allSupplies.length,
|
||||
fulfillmentOwnOrdersCount: fulfillmentOwnOrders.length,
|
||||
fulfillmentProductNames: Array.from(fulfillmentProductNames),
|
||||
filteredSellerSuppliesCount: sellerSupplies.length,
|
||||
sellerOrdersCount: sellerSupplyOrders.length,
|
||||
suppliesCount: transformedSupplies.length,
|
||||
supplies: transformedSupplies.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
pricePerUnit: s.pricePerUnit,
|
||||
warehouseStock: s.warehouseStock,
|
||||
isAvailable: s.isAvailable,
|
||||
})),
|
||||
})
|
||||
|
||||
// Возвращаем только расходники селлеров (исключая расходники фулфилмента)
|
||||
return sellerSupplies
|
||||
return transformedSupplies
|
||||
},
|
||||
|
||||
// Расходники фулфилмента (материалы для работы фулфилмента)
|
||||
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
|
||||
// Доступные расходники для рецептур селлеров (только с ценой и в наличии)
|
||||
getAvailableSuppliesForRecipe: async (_: unknown, __: unknown, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
@ -778,83 +747,90 @@ export const resolvers = {
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
}
|
||||
|
||||
// TypeScript assertion - мы знаем что organization не null после проверки выше
|
||||
const organization = currentUser.organization
|
||||
// Селлеры могут получать расходники от своих фулфилмент-партнеров
|
||||
if (currentUser.organization.type !== 'SELLER') {
|
||||
return [] // Только селлеры используют рецептуры
|
||||
}
|
||||
|
||||
// Получаем заказы поставок, созданные этим фулфилмент-центром для себя
|
||||
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
|
||||
// TODO: В будущем здесь будет логика получения расходников от партнерских фулфилментов
|
||||
// Пока возвращаем пустой массив, так как эта функциональность еще разрабатывается
|
||||
console.warn('🔥 getAvailableSuppliesForRecipe called for seller:', {
|
||||
sellerId: currentUser.organization.id,
|
||||
sellerName: currentUser.organization.name,
|
||||
})
|
||||
|
||||
return []
|
||||
},
|
||||
|
||||
// Расходники фулфилмента из склада (новая архитектура - синхронизация со склада)
|
||||
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
|
||||
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
|
||||
|
||||
if (!context.user) {
|
||||
console.warn('❌ No user in context')
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
console.warn('👤 Current user:', {
|
||||
id: currentUser?.id,
|
||||
phone: currentUser?.phone,
|
||||
organizationId: currentUser?.organizationId,
|
||||
organizationType: currentUser?.organization?.type,
|
||||
organizationName: currentUser?.organization?.name,
|
||||
})
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
console.warn('❌ No organization for user')
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
}
|
||||
|
||||
// Проверяем что это фулфилмент центр
|
||||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||||
console.warn('❌ User organization is not FULFILLMENT:', currentUser.organization.type)
|
||||
throw new GraphQLError('Доступ только для фулфилмент центров')
|
||||
}
|
||||
|
||||
// Получаем расходники фулфилмента из таблицы Supply
|
||||
const supplies = await prisma.supply.findMany({
|
||||
where: {
|
||||
organizationId: organization.id, // Создали мы
|
||||
fulfillmentCenterId: organization.id, // Получатель - мы
|
||||
status: {
|
||||
in: ['PENDING', 'CONFIRMED', 'IN_TRANSIT', 'DELIVERED'], // Все статусы
|
||||
},
|
||||
organizationId: currentUser.organization.id,
|
||||
type: 'FULFILLMENT_CONSUMABLES', // Только расходники фулфилмента
|
||||
},
|
||||
include: {
|
||||
partner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
organization: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
// Преобразуем заказы поставок в формат supply для единообразия
|
||||
const fulfillmentSupplies = fulfillmentSupplyOrders.flatMap((order) =>
|
||||
order.items.map((item) => ({
|
||||
id: `fulfillment-order-${order.id}-${item.id}`,
|
||||
name: item.product.name,
|
||||
description: item.product.description || `Расходники от ${order.partner.name}`,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
unit: 'шт',
|
||||
category: item.product.category?.name || 'Расходники фулфилмента',
|
||||
status:
|
||||
order.status === 'PENDING'
|
||||
? 'planned'
|
||||
: order.status === 'CONFIRMED'
|
||||
? 'confirmed'
|
||||
: order.status === 'IN_TRANSIT'
|
||||
? 'in-transit'
|
||||
: order.status === 'DELIVERED'
|
||||
? 'in-stock'
|
||||
: 'planned',
|
||||
date: order.createdAt,
|
||||
supplier: order.partner.name || order.partner.fullName || 'Не указан',
|
||||
minStock: Math.round(item.quantity * 0.1),
|
||||
currentStock: order.status === 'DELIVERED' ? item.quantity : 0,
|
||||
usedStock: 0, // TODO: Подсчитывать реальное использование
|
||||
imageUrl: null,
|
||||
createdAt: order.createdAt,
|
||||
updatedAt: order.updatedAt,
|
||||
organizationId: organization.id,
|
||||
organization: organization,
|
||||
shippedQuantity: 0,
|
||||
})),
|
||||
)
|
||||
|
||||
// Логирование для отладки
|
||||
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥')
|
||||
console.warn('📊 Расходники фулфилмента:', {
|
||||
organizationId: organization.id,
|
||||
organizationType: organization.type,
|
||||
fulfillmentOrdersCount: fulfillmentSupplyOrders.length,
|
||||
fulfillmentSuppliesCount: fulfillmentSupplies.length,
|
||||
fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({
|
||||
id: o.id,
|
||||
supplierName: o.partner.name,
|
||||
status: o.status,
|
||||
itemsCount: o.items.length,
|
||||
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
|
||||
console.warn('📊 Расходники фулфилмента из склада:', {
|
||||
organizationId: currentUser.organization.id,
|
||||
organizationType: currentUser.organization.type,
|
||||
suppliesCount: supplies.length,
|
||||
supplies: supplies.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
type: s.type,
|
||||
status: s.status,
|
||||
currentStock: s.currentStock,
|
||||
quantity: s.quantity,
|
||||
})),
|
||||
})
|
||||
|
||||
return fulfillmentSupplies
|
||||
// Преобразуем в формат для фронтенда
|
||||
return supplies.map(supply => ({
|
||||
...supply,
|
||||
price: supply.price ? parseFloat(supply.price.toString()) : 0,
|
||||
shippedQuantity: 0, // Добавляем для совместимости
|
||||
}))
|
||||
},
|
||||
|
||||
// Заказы поставок расходников
|
||||
@ -1411,11 +1387,6 @@ export const resolvers = {
|
||||
|
||||
// Мои товары и расходники (для поставщиков)
|
||||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||||
console.warn('🔍 MY_PRODUCTS RESOLVER - ВЫЗВАН:', {
|
||||
hasUser: !!context.user,
|
||||
userId: context.user?.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
@ -1428,12 +1399,6 @@ export const resolvers = {
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
console.warn('👤 ПОЛЬЗОВАТЕЛЬ НАЙДЕН:', {
|
||||
userId: currentUser?.id,
|
||||
hasOrganization: !!currentUser?.organization,
|
||||
organizationType: currentUser?.organization?.type,
|
||||
organizationName: currentUser?.organization?.name,
|
||||
})
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
@ -1441,10 +1406,6 @@ export const resolvers = {
|
||||
|
||||
// Проверяем, что это поставщик
|
||||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||||
console.warn('❌ ДОСТУП ЗАПРЕЩЕН - НЕ ПОСТАВЩИК:', {
|
||||
actualType: currentUser.organization.type,
|
||||
requiredType: 'WHOLESALE',
|
||||
})
|
||||
throw new GraphQLError('Товары доступны только для поставщиков')
|
||||
}
|
||||
|
||||
@ -2586,6 +2547,7 @@ export const resolvers = {
|
||||
bik?: string
|
||||
accountNumber?: string
|
||||
corrAccount?: string
|
||||
market?: string
|
||||
}
|
||||
},
|
||||
context: Context,
|
||||
@ -2636,6 +2598,7 @@ export const resolvers = {
|
||||
emails?: object
|
||||
managementName?: string
|
||||
managementPost?: string
|
||||
market?: string
|
||||
} = {}
|
||||
|
||||
// Название организации больше не обновляется через профиль
|
||||
@ -2651,6 +2614,11 @@ export const resolvers = {
|
||||
updateData.emails = [{ value: input.email, type: 'main' }]
|
||||
}
|
||||
|
||||
// Обновляем рынок для поставщиков
|
||||
if (input.market !== undefined) {
|
||||
updateData.market = input.market === 'none' ? null : input.market
|
||||
}
|
||||
|
||||
// Сохраняем дополнительные контакты в custom полях
|
||||
// Пока добавим их как дополнительные JSON поля
|
||||
const customContacts: {
|
||||
@ -3639,23 +3607,14 @@ export const resolvers = {
|
||||
}
|
||||
},
|
||||
|
||||
// Создать расходник
|
||||
createSupply: async (
|
||||
|
||||
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
|
||||
updateSupplyPrice: async (
|
||||
_: unknown,
|
||||
args: {
|
||||
id: string
|
||||
input: {
|
||||
name: string
|
||||
description?: string
|
||||
price: number
|
||||
quantity: number
|
||||
unit: string
|
||||
category: string
|
||||
status: string
|
||||
date: string
|
||||
supplier: string
|
||||
minStock: number
|
||||
currentStock: number
|
||||
imageUrl?: string
|
||||
pricePerUnit?: number | null
|
||||
}
|
||||
},
|
||||
context: Context,
|
||||
@ -3677,167 +3636,68 @@ export const resolvers = {
|
||||
|
||||
// Проверяем, что это фулфилмент центр
|
||||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||||
throw new GraphQLError('Расходники доступны только для фулфилмент центров')
|
||||
throw new GraphQLError('Обновление цен расходников доступно только для фулфилмент центров')
|
||||
}
|
||||
|
||||
try {
|
||||
const supply = await prisma.supply.create({
|
||||
data: {
|
||||
name: args.input.name,
|
||||
description: args.input.description,
|
||||
price: args.input.price,
|
||||
quantity: args.input.quantity,
|
||||
unit: args.input.unit,
|
||||
category: args.input.category,
|
||||
status: args.input.status,
|
||||
date: new Date(args.input.date),
|
||||
supplier: args.input.supplier,
|
||||
minStock: args.input.minStock,
|
||||
currentStock: args.input.currentStock,
|
||||
imageUrl: args.input.imageUrl,
|
||||
// Находим и обновляем расходник
|
||||
const existingSupply = await prisma.supply.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
organizationId: currentUser.organization.id,
|
||||
},
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Расходник успешно создан',
|
||||
supply,
|
||||
if (!existingSupply) {
|
||||
throw new GraphQLError('Расходник не найден')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating supply:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Ошибка при создании расходника',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Обновить расходник
|
||||
updateSupply: async (
|
||||
_: unknown,
|
||||
args: {
|
||||
id: string
|
||||
input: {
|
||||
name: string
|
||||
description?: string
|
||||
price: number
|
||||
quantity: number
|
||||
unit: string
|
||||
category: string
|
||||
status: string
|
||||
date: string
|
||||
supplier: string
|
||||
minStock: number
|
||||
currentStock: number
|
||||
imageUrl?: string
|
||||
}
|
||||
},
|
||||
context: Context,
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
}
|
||||
|
||||
// Проверяем, что расходник принадлежит текущей организации
|
||||
const existingSupply = await prisma.supply.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
organizationId: currentUser.organization.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existingSupply) {
|
||||
throw new GraphQLError('Расходник не найден или нет доступа')
|
||||
}
|
||||
|
||||
try {
|
||||
const supply = await prisma.supply.update({
|
||||
const updatedSupply = await prisma.supply.update({
|
||||
where: { id: args.id },
|
||||
data: {
|
||||
name: args.input.name,
|
||||
description: args.input.description,
|
||||
price: args.input.price,
|
||||
quantity: args.input.quantity,
|
||||
unit: args.input.unit,
|
||||
category: args.input.category,
|
||||
status: args.input.status,
|
||||
date: new Date(args.input.date),
|
||||
supplier: args.input.supplier,
|
||||
minStock: args.input.minStock,
|
||||
currentStock: args.input.currentStock,
|
||||
imageUrl: args.input.imageUrl,
|
||||
price: args.input.pricePerUnit, // Обновляем только цену
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
// Преобразуем в новый формат для GraphQL
|
||||
const transformedSupply = {
|
||||
id: updatedSupply.id,
|
||||
name: updatedSupply.name,
|
||||
description: updatedSupply.description,
|
||||
pricePerUnit: updatedSupply.price ? parseFloat(updatedSupply.price.toString()) : null, // Конвертируем Decimal в Number
|
||||
unit: updatedSupply.unit || 'шт',
|
||||
imageUrl: updatedSupply.imageUrl,
|
||||
warehouseStock: updatedSupply.currentStock || 0,
|
||||
isAvailable: (updatedSupply.currentStock || 0) > 0,
|
||||
warehouseConsumableId: updatedSupply.id,
|
||||
createdAt: updatedSupply.createdAt,
|
||||
updatedAt: updatedSupply.updatedAt,
|
||||
organization: updatedSupply.organization,
|
||||
}
|
||||
|
||||
console.warn('🔥 SUPPLY PRICE UPDATED:', {
|
||||
id: transformedSupply.id,
|
||||
name: transformedSupply.name,
|
||||
oldPrice: existingSupply.price,
|
||||
newPrice: transformedSupply.pricePerUnit,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Расходник успешно обновлен',
|
||||
supply,
|
||||
message: 'Цена расходника успешно обновлена',
|
||||
supply: transformedSupply,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating supply:', error)
|
||||
console.error('Error updating supply price:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Ошибка при обновлении расходника',
|
||||
message: 'Ошибка при обновлении цены расходника',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Удалить расходник
|
||||
deleteSupply: async (_: unknown, args: { id: string }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
}
|
||||
|
||||
// Проверяем, что расходник принадлежит текущей организации
|
||||
const existingSupply = await prisma.supply.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
organizationId: currentUser.organization.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existingSupply) {
|
||||
throw new GraphQLError('Расходник не найден или нет доступа')
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.supply.delete({
|
||||
where: { id: args.id },
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error deleting supply:', error)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// Использовать расходники фулфилмента
|
||||
useFulfillmentSupplies: async (
|
||||
_: unknown,
|
||||
|
Reference in New Issue
Block a user