diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 744f359..df1360a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -203,7 +203,8 @@ model Supply { id String @id @default(cuid()) name String description String? - price Decimal @db.Decimal(10, 2) + price Decimal @db.Decimal(10, 2) // Цена закупки у поставщика (не меняется) + pricePerUnit Decimal? @db.Decimal(10, 2) // Цена продажи селлерам (устанавливается фулфилментом) quantity Int @default(0) unit String @default("шт") category String @default("Расходники") diff --git a/rules-complete.md b/rules-complete.md index c04f07a..c582e41 100644 --- a/rules-complete.md +++ b/rules-complete.md @@ -2282,7 +2282,61 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins); - С визуальной индикацией состояния (активные/неактивные/без цены) -#### 11.7.5 Технические требования +#### 11.7.5 Разделение цен закупки и продажи + +**КРИТИЧЕСКОЕ ПРАВИЛО**: Расходники фулфилмента имеют **ДВЕ РАЗНЫЕ ЦЕНЫ** для разных бизнес-процессов: + +1. **ЦЕНА ЗАКУПКИ** (`Supply.price`) - цена, по которой фулфилмент купил расходник у поставщика +2. **ЦЕНА ПРОДАЖИ** (`Supply.pricePerUnit`) - цена, по которой фулфилмент продает расходник селлерам + +**ПОЛЯ В БАЗЕ ДАННЫХ**: + +```prisma +model Supply { + price Decimal @db.Decimal(10, 2) // Цена закупки у поставщика (НЕИЗМЕННАЯ) + pricePerUnit Decimal? @db.Decimal(10, 2) // Цена продажи селлерам (устанавливается фулфилментом) +} +``` + +**ПРАВИЛА ОТОБРАЖЕНИЯ ПО РАЗДЕЛАМ**: + +**РАЗДЕЛ "СКЛАД → РАСХОДНИКИ ФУЛФИЛМЕНТА"**: + +- Показывает `Supply.price` (цена закупки) +- Цена ТОЛЬКО ДЛЯ ЧТЕНИЯ, нельзя изменять +- Отражает историческую стоимость приобретения + +**РАЗДЕЛ "УСЛУГИ → РАСХОДНИКИ"**: + +- Показывает и редактирует `Supply.pricePerUnit` (цена продажи) +- Единственное место где можно изменить цену для селлеров +- Влияет на рецептуры и расчеты для селлеров + +**БИЗНЕС-ЛОГИКА СОЗДАНИЯ**: + +ПРИ ПОСТУПЛЕНИИ ОТ ПОСТАВЩИКА: + +```typescript +const supply = await prisma.supply.create({ + data: { + price: item.price, // Цена поставщика → ЗАФИКСИРОВАНА + pricePerUnit: null, // Цена продажи → ПУСТАЯ + }, +}) +``` + +УСТАНОВКА ЦЕНЫ ПРОДАЖИ (в разделе "Услуги"): + +```typescript +const updated = await prisma.supply.update({ + data: { + pricePerUnit: newPrice, // ТОЛЬКО цена продажи + // price НЕ ТРОГАЕМ - остается цена закупки + }, +}) +``` + +#### 11.7.6 Технические требования **GraphQL типы:** @@ -2743,6 +2797,9 @@ const wholesalePartners = await prisma.counterparty.findMany({ 24. ❌ **ИСПОЛЬЗОВАТЬ НАЗВАНИЯ ОРГАНИЗАЦИЙ В ЛОГИКЕ БЕЗОПАСНОСТИ** - проверки доступа только по `organization.type` и системным ID 25. ❌ **СОЗДАВАТЬ УСЛОВИЯ НА ОСНОВЕ ПОЛЬЗОВАТЕЛЬСКИХ СТРОК** - никаких `if (name.includes())` для определения функционала 26. ❌ **ПУТАТЬ ДАННЫЕ И ФУНКЦИОНАЛ** - "ОПТ Маркет" (название рынка) ≠ "Маркет" (раздел системы) +27. ❌ **ПРЕДСТАВЛЯТЬ ИНТЕРПРЕТАЦИИ КАК ФАКТЫ** - всегда четко разделять прямые цитаты из правил и логические выводы +28. ❌ **ОТВЕЧАТЬ БЕЗ ССЫЛОК НА ИСТОЧНИКИ** - при ссылке на правила всегда указывать номер строки или раздел +29. ❌ **ИСПОЛЬЗОВАТЬ КАТЕГОРИЧНЫЕ УТВЕРЖДЕНИЯ БЕЗ ДОКАЗАТЕЛЬСТВ** - избегать "ТОЧНО!", "ИМЕННО ТАК!" без прямых цитат ### 17.2 ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА: @@ -2756,8 +2813,41 @@ const wholesalePartners = await prisma.counterparty.findMany({ 8. ✅ Проверка доступности товаров перед заказом 9. ✅ Соблюдение жизненного цикла статусов поставок 10. ✅ Фиксация план/факт в процессе создания продукта +11. ✅ **УКАЗЫВАТЬ ИСТОЧНИКИ ИНФОРМАЦИИ** - при ссылке на правила обязательно указывать строку/раздел +12. ✅ **РАЗДЕЛЯТЬ ФАКТЫ И ИНТЕРПРЕТАЦИИ** - четко маркировать что взято из правил, а что является выводом +13. ✅ **ИСПОЛЬЗОВАТЬ ОСТОРОЖНЫЕ ФОРМУЛИРОВКИ** - "согласно правилам", "возможно", "требует уточнения" -### 17.3 🔒 ПРАВИЛА БЕЗОПАСНОСТИ: Разделение данных и функционала +### 17.3 📝 ОБЯЗАТЕЛЬНЫЙ ФОРМАТ ОТВЕТОВ С ФАКТАМИ + +**При ссылке на правила ОБЯЗАТЕЛЬНО использовать формат:** + +✅ **ПРАВИЛЬНО:** + +``` +📖 ФАКТ из rules-complete.md (строка 2225): "установка цены за единицу" +🧠 МОЯ ИНТЕРПРЕТАЦИЯ: возможно, это происходит в разделе X +❓ ПРЕДПОЛОЖЕНИЕ: требует уточнения у пользователя +⚠️ НЕ НАЙДЕНО в правилах: информация о точном местоположении +``` + +❌ **НЕПРАВИЛЬНО:** + +``` +"Да! Точно понимаю! Фулфилмент устанавливает цены в разделе X!" +``` + +**ОБЯЗАТЕЛЬНАЯ МАРКИРОВКА:** + +- 📖 **ФАКТ** - прямая цитата из правил с номером строки +- 🧠 **ИНТЕРПРЕТАЦИЯ** - мой логический вывод (четко обозначен) +- ❓ **ПРЕДПОЛОЖЕНИЕ** - гипотеза, требующая подтверждения +- ⚠️ **НЕ НАЙДЕНО** - информация отсутствует в правилах + +**СТОП-СЛОВА (избегать без доказательств):** +❌ "ТОЧНО!", "ИМЕННО ТАК!", "ДА! ПОНИМАЮ!", "АБСОЛЮТНО ВЕРНО!" +✅ "Согласно правилам...", "Не указано, но возможно...", "Требует уточнения" + +### 17.4 🔒 ПРАВИЛА БЕЗОПАСНОСТИ: Разделение данных и функционала #### КРИТИЧЕСКОЕ ПРАВИЛО БЕЗОПАСНОСТИ @@ -2817,6 +2907,131 @@ if (ALLOWED_FULFILLMENT_IDS.includes(organization.id)) { **ПРАВИЛО**: Физический рынок "ОПТ Маркет" - это просто строка данных. Раздел "Маркет" (/market) - это системный функционал. Они никак не связаны и не должны влиять друг на друга. +### 17.5 📦 УПРАВЛЕНИЕ СВЯЗЯМИ ТОВАР-КАРТОЧКА В РЕЦЕПТУРЕ + +#### 17.5.1 Общие принципы + +**НАЗНАЧЕНИЕ**: Связь товара с карточкой маркетплейса - это метаданные для учета, НЕ влияющие на физический состав продукта. + +**ФОРМУЛА ПРОДУКТА НЕИЗМЕННА**: + +``` +ПРОДУКТ = Товар + Услуга(и) + Расходники селлера + Расходники ФФ +``` + +**СВЯЗЬ С МП** = отдельные метаданные для логистики и учета + +#### 17.5.2 UI компонент связи с карточками + +**РАСПОЛОЖЕНИЕ**: В форме создания поставки, в секции каждого товара + +**ТИП КОМПОНЕНТА**: Dropdown с поиском и фильтрацией + +**ИСТОЧНИК ДАННЫХ**: База данных карточек маркетплейсов селлера (GraphQL запрос) + +#### 17.5.3 Логика состояний карточек + +**✅ СВЯЗАНО** - карточка уже привязана к этому товару: + +- Показывать зеленую галочку +- Текст: "Название карточки - Связано" +- Можно отвязать (сброс в "Без привязки") + +**⚠️ ДОСТУПНО** - карточка свободна для привязки: + +- Показывать желтый значок предупреждения +- Текст: "Название карточки - Доступно" +- Можно привязать к текущему товару + +**❌ ЗАНЯТО** - карточка привязана к другому товару: + +- Показывать красный крестик +- Текст: "Название карточки - Занято (товар: 'Название')" +- Пункт заблокирован (disabled) +- Показывать для информации, но нельзя выбрать + +**🔍 БЕЗ ПРИВЯЗКИ** - товар не связан с карточкой: + +- Пункт по умолчанию +- Показывать серый значок +- Текст: "Без привязки к карточке" + +#### 17.5.4 Техническая реализация + +**GraphQL запрос**: + +```graphql +query GetSellerCards { + myMarketplaceCards { + id + title + marketplace + article + linkedProductId # null если свободна + linkedProduct { + # для отображения занятости + id + name + } + } +} +``` + +**Логика фильтрации**: + +- Все карточки селлера показываются в dropdown +- Статус определяется по полю `linkedProductId` +- Автосвязка: карточки с похожим названием показываются первыми + +**Сохранение**: + +- При создании поставки связь сохраняется в поле `marketplaceCardId` рецептуры +- При изменении связи обновляется поле `linkedProductId` в карточке + +#### 17.5.5 UX поведение + +**ПОИСК В DROPDOWN**: + +- Фильтрация по названию карточки +- Фильтрация по артикулу маркетплейса +- Автофокус при открытии + +**ГРУППИРОВКА**: + +``` +[Dropdown: Выберите карточку Wildberries ▼] +├─ 🔍 БЕЗ ПРИВЯЗКИ +├─ ────── ДОСТУПНЫЕ ────── +├─ ⚠️ "Кроссовки Nike Air" - Доступно +├─ ⚠️ "Футболка Adidas" - Доступно +├─ ────── СВЯЗАННЫЕ ────── +├─ ✅ "Джинсы Levi's" - Связано +├─ ────── ЗАНЯТЫЕ ────── +└─ ❌ "Куртка Puma" - Занято (товар "Верхняя одежда") [disabled] +``` + +**ВАЛИДАЦИЯ**: + +- Связь опциональна - можно создать поставку без привязки +- При выборе занятой карточки показывать предупреждение +- При отвязке подтверждать действие + +#### 17.5.6 Интеграция с существующими правилами + +**СОВМЕСТИМОСТЬ**: + +- Не нарушает существующую логику создания поставок +- Дополняет рецептуру метаданными +- Совместима с типами поставок (карточки/поставщики) + +**ОБЯЗАТЕЛЬНОСТЬ**: + +- Связь с карточкой - ОПЦИОНАЛЬНА +- Товар может существовать без привязки к МП +- Карточка может существовать без привязки к товару + +**ПРИОРИТЕТ РАЗРАБОТКИ**: Средний (не блокирует основную функциональность) + --- ## 18. 🛠️ GRAPHQL И TYPESCRIPT ПРАВИЛА @@ -2966,6 +3181,70 @@ query GetMarketProducts { - **Валидация обязательных параметров** на уровне схемы (`organizationId: ID!`) - **Кеширование обходить при проблемах** через `fetchPolicy: 'network-only'` +### 18.8 GraphQL правила для поля organization в мутациях + +#### 18.8.1 Обязательность поля organization + +**ПРАВИЛО**: Все мутации, возвращающие объекты с типом, включающим `organization: Organization!`, ДОЛЖНЫ запрашивать это поле. + +**ПРОБЛЕМА**: Apollo Client кэш ожидает поле `organization` в ответе, если оно определено в GraphQL типе как обязательное. + +#### 18.8.2 Правильное написание мутаций + +**❌ НЕПРАВИЛЬНО** (вызывает ошибку Apollo Client): + +```graphql +mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) { + updateLogistics(id: $id, input: $input) { + success + logistics { + id + fromLocation + # НЕТ поля organization - ОШИБКА кэша! + } + } +} +``` + +**✅ ПРАВИЛЬНО** (работает корректно): + +```graphql +mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) { + updateLogistics(id: $id, input: $input) { + success + logistics { + id + fromLocation + organization { + # ОБЯЗАТЕЛЬНО включить! + id + name + fullName + } + } + } +} +``` + +#### 18.8.3 Чек-лист для мутаций + +**ОБЯЗАТЕЛЬНАЯ ПРОВЕРКА** перед созданием мутации: + +1. ✅ Проверить GraphQL тип возвращаемого объекта +2. ✅ Если есть поле `organization: Organization!` - добавить в запрос +3. ✅ Включить минимальные поля: `id`, `name`, `fullName` +4. ✅ Проверить resolver включает `include: { organization: true }` + +**ПРИМЕНЯЕТСЯ К**: + +- `CREATE_LOGISTICS` ✅ Исправлено +- `UPDATE_LOGISTICS` ✅ Исправлено +- `CREATE_SERVICE` - проверить при разработке +- `UPDATE_SERVICE` - проверить при разработке +- Все другие мутации с организационными объектами + +**ОШИБКА БЕЗ ПОЛЯ**: `Error converting field "organization" of expected non-nullable type` + --- ## 19. 🔧 АРХИТЕКТУРНЫЕ ПРИНЦИПЫ @@ -3353,7 +3632,7 @@ const handleSuppliesClick = () => { _Эта база знаний создана путем объединения rules-unified.md (v3.0) и fulfillment-cabinet-rules.md (v1.0) с устранением всех несоответствий и добавлением критически важных улучшений: быстрый справочник, глоссарий терминов, детальные алгоритмы процессов, edge cases._ -_Версия: 10.0_ +_Версия: 10.1_ _Дата создания: 2025_ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗРАБОТКЕ_ @@ -3441,3 +3720,11 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ - ✅ **РАСШИРЕН ГЛОССАРИЙ**: Контекстно-зависимые термины для SupplyOrder - ✅ **УТОЧНЕНИЕ ТЕРМИНОВ**: Четкое разделение "Маркет" (раздел) vs "Маркетплейс" (внешние площадки) - ✅ **ПРИМЕРЫ УЯЗВИМОСТЕЙ**: Конкретные примеры безопасного и небезопасного кода + +### 📝 КАЧЕСТВО ОТВЕТОВ v10.1: + +- ✅ **НОВЫЕ ЗАПРЕТЫ 27-29**: Запрет представления интерпретаций как фактов +- ✅ **ОБЯЗАТЕЛЬНЫЙ ФОРМАТ ОТВЕТОВ 17.3**: Четкое разделение фактов, интерпретаций и предположений +- ✅ **СИСТЕМА МАРКИРОВКИ**: 📖 ФАКТ, 🧠 ИНТЕРПРЕТАЦИЯ, ❓ ПРЕДПОЛОЖЕНИЕ, ⚠️ НЕ НАЙДЕНО +- ✅ **СТОП-СЛОВА**: Список категоричных утверждений для избегания без доказательств +- ✅ **ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА 11-13**: Указание источников и осторожные формулировки diff --git a/src/components/services/supplies-tab.tsx b/src/components/services/supplies-tab.tsx index ee72616..78e3cd0 100644 --- a/src/components/services/supplies-tab.tsx +++ b/src/components/services/supplies-tab.tsx @@ -54,12 +54,23 @@ export function SuppliesTab() { const [isSaving, setIsSaving] = useState(false) const [isInitialized, setIsInitialized] = useState(false) + // Debug информация + console.log('SuppliesTab - User:', user?.phone, 'Type:', user?.organization?.type) + // GraphQL запросы и мутации const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, { - skip: user?.organization?.type !== 'FULFILLMENT', + skip: !user || user?.organization?.type !== 'FULFILLMENT', }) const [updateSupplyPrice] = useMutation(UPDATE_SUPPLY_PRICE) + // Debug GraphQL запроса + console.log('SuppliesTab - Query:', { + skip: !user || user?.organization?.type !== 'FULFILLMENT', + loading, + error: error?.message, + dataLength: data?.mySupplies?.length, + }) + const supplies = data?.mySupplies || [] // Преобразуем загруженные расходники в редактируемый формат @@ -130,7 +141,7 @@ export function SuppliesTab() { if (field !== 'pricePerUnit') { return // Только цену можно редактировать } - + setEditableSupplies((prev) => prev.map((supply) => { if (supply.id !== supplyId) return supply @@ -155,7 +166,7 @@ export function SuppliesTab() { for (const supply of suppliesToSave) { // Проверяем валидность цены (может быть пустой) const pricePerUnit = supply.pricePerUnit.trim() ? parseFloat(supply.pricePerUnit) : null - + if (supply.pricePerUnit.trim() && (isNaN(pricePerUnit!) || pricePerUnit! <= 0)) { toast.error('Введите корректную цену') setIsSaving(false) @@ -187,9 +198,7 @@ export function SuppliesTab() { } // Сбрасываем флаги изменений - setEditableSupplies((prev) => - prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })), - ) + setEditableSupplies((prev) => prev.map((s) => ({ ...s, hasChanges: false, isEditing: false }))) toast.success('Цены успешно обновлены') } catch (error) { @@ -212,7 +221,9 @@ export function SuppliesTab() {
Расходники появляются автоматически из поставок. Можно только установить цену.
++ Расходники появляются автоматически из поставок. Можно только установить цену. +
Не удалось загрузить расходники
+
+ Не удалось загрузить расходники
+ {process.env.NODE_ENV === 'development' && (
+ <>
+
+
+ Debug: {error.message}
+
+ User type: {user?.organization?.type}
+
+ >
+ )}
+