feat(supplier-orders): добавить параметры поставки в таблицу заявок

- Добавлены колонки Объём и Грузовые места между Цена товаров и Статус
- Реализованы инпуты для ввода volume и packagesCount в статусе PENDING для роли WHOLESALE
- Добавлена мутация UPDATE_SUPPLY_PARAMETERS с проверками безопасности
- Скрыта строка Поставщик для роли WHOLESALE (поставщик знает свои данные)
- Исправлено выравнивание таблицы при скрытии уровня поставщика
- Реорганизованы документы: legacy-rules/, docs/, docs-and-reports/

ВНИМАНИЕ: Компонент multilevel-supplies-table.tsx (1697 строк) нарушает правило модульной архитектуры (>800 строк требует рефакторинга)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-23 18:47:23 +03:00
parent 35cbbac504
commit 12fd8ddf61
27 changed files with 1250 additions and 208 deletions

View File

@ -15,6 +15,18 @@ import '@/lib/seed-init' // Автоматическая инициализац
// 🔒 СИСТЕМА БЕЗОПАСНОСТИ - импорты
import { CommercialDataAudit } from './security/commercial-data-audit'
import { createSecurityContext } from './security/index'
// 🔒 HELPER: Создание безопасного контекста с организационными данными
function createSecureContextWithOrgData(context: Context, currentUser: any) {
return {
...context,
user: {
...context.user,
organizationType: currentUser.organization.type,
organizationId: currentUser.organization.id,
}
}
}
import { ParticipantIsolation } from './security/participant-isolation'
import { SupplyDataFilter } from './security/supply-data-filter'
import type { SecurityContext } from './security/types'
@ -2735,15 +2747,17 @@ export const resolvers = {
: []
// 🔒 ФИЛЬТРАЦИЯ РЕЦЕПТУРЫ ПО РОЛИ
recipe = SupplyDataFilter.filterRecipeByRole(
{
// Для WHOLESALE скрываем рецептуру полностью
if (currentUser.organization.type === 'WHOLESALE') {
recipe = null
} else {
recipe = {
services,
fulfillmentConsumables,
sellerConsumables,
marketplaceCardId: item.marketplaceCardId,
},
securityContext,
)
}
}
}
return {
@ -7269,6 +7283,67 @@ export const resolvers = {
}
},
// Обновление параметров поставки (объём и грузовые места)
updateSupplyParameters: async (
_: unknown,
args: { id: string; volume?: number; packagesCount?: number },
context: GraphQLContext
) => {
try {
// Проверка аутентификации
if (!context.user?.id) {
return {
success: false,
message: 'Необходима аутентификация',
}
}
// Найти поставку и проверить права доступа
const supply = await prisma.supplyOrder.findUnique({
where: { id: args.id },
include: { partner: true }
})
if (!supply) {
return {
success: false,
message: 'Поставка не найдена',
}
}
// Проверить, что пользователь - поставщик этой заявки
if (supply.partnerId !== context.user.organization?.id) {
return {
success: false,
message: 'Недостаточно прав для изменения данной поставки',
}
}
// Подготовить данные для обновления
const updateData: { volume?: number; packagesCount?: number } = {}
if (args.volume !== undefined) updateData.volume = args.volume
if (args.packagesCount !== undefined) updateData.packagesCount = args.packagesCount
// Обновить поставку
const updatedSupply = await prisma.supplyOrder.update({
where: { id: args.id },
data: updateData,
})
return {
success: true,
message: 'Параметры поставки обновлены',
order: updatedSupply,
}
} catch (error) {
console.error('Ошибка при обновлении параметров поставки:', error)
return {
success: false,
message: 'Ошибка при обновлении параметров поставки',
}
}
},
// Назначение логистики фулфилментом на заказ селлера
assignLogisticsToSupply: async (
_: unknown,
@ -7543,22 +7618,34 @@ export const resolvers = {
organization: true,
},
},
recipe: {
include: {
services: true,
fulfillmentConsumables: true,
sellerConsumables: true,
},
},
},
},
},
})
console.warn(`[DEBUG] updatedOrder structure:`, {
id: updatedOrder.id,
itemsCount: updatedOrder.items?.length || 0,
firstItem: updatedOrder.items?.[0] ? {
productId: updatedOrder.items[0].productId,
hasProduct: !!updatedOrder.items[0].product,
productOrgId: updatedOrder.items[0].product?.organizationId,
hasProductOrg: !!updatedOrder.items[0].product?.organization,
} : null,
currentUserOrgId: currentUser.organization.id,
})
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContext)
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
console.warn(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`)
console.warn(`[DEBUG] filteredOrder:`, {
hasData: !!filteredOrder.data,
dataId: filteredOrder.data?.id,
dataKeys: Object.keys(filteredOrder.data || {}),
})
try {
const orgIds = [
updatedOrder.organizationId,
@ -7572,10 +7659,16 @@ export const resolvers = {
})
} catch {}
// Проверка на случай, если фильтрованные данные null
if (!filteredOrder.data || !filteredOrder.data.id) {
console.error('[ERROR] filteredOrder.data is null or missing id:', filteredOrder)
throw new GraphQLError('Filtered order data is invalid')
}
return {
success: true,
message: 'Заказ поставки одобрен поставщиком. Товары зарезервированы, остатки обновлены.',
order: filteredOrder, // 🔒 Возвращаем отфильтрованные данные
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
}
} catch (error) {
console.error('Error approving supply order:', error)
@ -7689,20 +7782,14 @@ export const resolvers = {
organization: true,
},
},
recipe: {
include: {
services: true,
fulfillmentConsumables: true,
sellerConsumables: true,
},
},
},
},
},
})
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContext)
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
// 📦 СНИМАЕМ РЕЗЕРВАЦИЮ ПРИ ОТКЛОНЕНИИ
// Восстанавливаем остатки и убираем резервацию для каждого отклоненного товара
@ -7756,7 +7843,7 @@ export const resolvers = {
return {
success: true,
message: args.reason ? `Заказ отклонен поставщиком. Причина: ${args.reason}` : 'Заказ отклонен поставщиком',
order: filteredOrder, // 🔒 Возвращаем отфильтрованные данные
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
}
} catch (error) {
console.error('Error rejecting supply order:', error)
@ -7914,7 +8001,8 @@ export const resolvers = {
})
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContext)
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
try {
const orgIds = [
@ -7932,7 +8020,7 @@ export const resolvers = {
return {
success: true,
message: "Заказ отправлен поставщиком. Товары переведены в статус 'в пути'.",
order: filteredOrder, // 🔒 Возвращаем отфильтрованные данные
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
}
} catch (error) {
console.error('Error shipping supply order:', error)