Fix fulfillment consumables pricing architecture
- Add pricePerUnit field to Supply model for seller pricing - Fix updateSupplyPrice mutation to update pricePerUnit only - Separate purchase price (price) from selling price (pricePerUnit) - Fix GraphQL mutations to include organization field (CREATE/UPDATE_LOGISTICS) - Update GraphQL types to make Supply.price required again - Add comprehensive pricing rules to rules-complete.md sections 11.7.5 and 18.8 - Fix supplies-tab.tsx to show debug info and handle user loading Architecture changes: • Supply.price = purchase price from supplier (immutable) • Supply.pricePerUnit = selling price to sellers (mutable by fulfillment) • Warehouse shows purchase price only (readonly) • Services shows/edits selling price only 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -203,7 +203,8 @@ model Supply {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
description 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)
|
quantity Int @default(0)
|
||||||
unit String @default("шт")
|
unit String @default("шт")
|
||||||
category String @default("Расходники")
|
category String @default("Расходники")
|
||||||
|
@ -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 типы:**
|
**GraphQL типы:**
|
||||||
|
|
||||||
@ -2743,6 +2797,9 @@ const wholesalePartners = await prisma.counterparty.findMany({
|
|||||||
24. ❌ **ИСПОЛЬЗОВАТЬ НАЗВАНИЯ ОРГАНИЗАЦИЙ В ЛОГИКЕ БЕЗОПАСНОСТИ** - проверки доступа только по `organization.type` и системным ID
|
24. ❌ **ИСПОЛЬЗОВАТЬ НАЗВАНИЯ ОРГАНИЗАЦИЙ В ЛОГИКЕ БЕЗОПАСНОСТИ** - проверки доступа только по `organization.type` и системным ID
|
||||||
25. ❌ **СОЗДАВАТЬ УСЛОВИЯ НА ОСНОВЕ ПОЛЬЗОВАТЕЛЬСКИХ СТРОК** - никаких `if (name.includes())` для определения функционала
|
25. ❌ **СОЗДАВАТЬ УСЛОВИЯ НА ОСНОВЕ ПОЛЬЗОВАТЕЛЬСКИХ СТРОК** - никаких `if (name.includes())` для определения функционала
|
||||||
26. ❌ **ПУТАТЬ ДАННЫЕ И ФУНКЦИОНАЛ** - "ОПТ Маркет" (название рынка) ≠ "Маркет" (раздел системы)
|
26. ❌ **ПУТАТЬ ДАННЫЕ И ФУНКЦИОНАЛ** - "ОПТ Маркет" (название рынка) ≠ "Маркет" (раздел системы)
|
||||||
|
27. ❌ **ПРЕДСТАВЛЯТЬ ИНТЕРПРЕТАЦИИ КАК ФАКТЫ** - всегда четко разделять прямые цитаты из правил и логические выводы
|
||||||
|
28. ❌ **ОТВЕЧАТЬ БЕЗ ССЫЛОК НА ИСТОЧНИКИ** - при ссылке на правила всегда указывать номер строки или раздел
|
||||||
|
29. ❌ **ИСПОЛЬЗОВАТЬ КАТЕГОРИЧНЫЕ УТВЕРЖДЕНИЯ БЕЗ ДОКАЗАТЕЛЬСТВ** - избегать "ТОЧНО!", "ИМЕННО ТАК!" без прямых цитат
|
||||||
|
|
||||||
### 17.2 ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА:
|
### 17.2 ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА:
|
||||||
|
|
||||||
@ -2756,8 +2813,41 @@ const wholesalePartners = await prisma.counterparty.findMany({
|
|||||||
8. ✅ Проверка доступности товаров перед заказом
|
8. ✅ Проверка доступности товаров перед заказом
|
||||||
9. ✅ Соблюдение жизненного цикла статусов поставок
|
9. ✅ Соблюдение жизненного цикла статусов поставок
|
||||||
10. ✅ Фиксация план/факт в процессе создания продукта
|
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) - это системный функционал. Они никак не связаны и не должны влиять друг на друга.
|
**ПРАВИЛО**: Физический рынок "ОПТ Маркет" - это просто строка данных. Раздел "Маркет" (/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 ПРАВИЛА
|
## 18. 🛠️ GRAPHQL И TYPESCRIPT ПРАВИЛА
|
||||||
@ -2966,6 +3181,70 @@ query GetMarketProducts {
|
|||||||
- **Валидация обязательных параметров** на уровне схемы (`organizationId: ID!`)
|
- **Валидация обязательных параметров** на уровне схемы (`organizationId: ID!`)
|
||||||
- **Кеширование обходить при проблемах** через `fetchPolicy: 'network-only'`
|
- **Кеширование обходить при проблемах** через `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. 🔧 АРХИТЕКТУРНЫЕ ПРИНЦИПЫ
|
## 19. 🔧 АРХИТЕКТУРНЫЕ ПРИНЦИПЫ
|
||||||
@ -3353,7 +3632,7 @@ const handleSuppliesClick = () => {
|
|||||||
|
|
||||||
_Эта база знаний создана путем объединения rules-unified.md (v3.0) и fulfillment-cabinet-rules.md (v1.0) с устранением всех несоответствий и добавлением критически важных улучшений: быстрый справочник, глоссарий терминов, детальные алгоритмы процессов, edge cases._
|
_Эта база знаний создана путем объединения rules-unified.md (v3.0) и fulfillment-cabinet-rules.md (v1.0) с устранением всех несоответствий и добавлением критически важных улучшений: быстрый справочник, глоссарий терминов, детальные алгоритмы процессов, edge cases._
|
||||||
|
|
||||||
_Версия: 10.0_
|
_Версия: 10.1_
|
||||||
_Дата создания: 2025_
|
_Дата создания: 2025_
|
||||||
_Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗРАБОТКЕ_
|
_Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗРАБОТКЕ_
|
||||||
|
|
||||||
@ -3441,3 +3720,11 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ
|
|||||||
- ✅ **РАСШИРЕН ГЛОССАРИЙ**: Контекстно-зависимые термины для SupplyOrder
|
- ✅ **РАСШИРЕН ГЛОССАРИЙ**: Контекстно-зависимые термины для SupplyOrder
|
||||||
- ✅ **УТОЧНЕНИЕ ТЕРМИНОВ**: Четкое разделение "Маркет" (раздел) vs "Маркетплейс" (внешние площадки)
|
- ✅ **УТОЧНЕНИЕ ТЕРМИНОВ**: Четкое разделение "Маркет" (раздел) vs "Маркетплейс" (внешние площадки)
|
||||||
- ✅ **ПРИМЕРЫ УЯЗВИМОСТЕЙ**: Конкретные примеры безопасного и небезопасного кода
|
- ✅ **ПРИМЕРЫ УЯЗВИМОСТЕЙ**: Конкретные примеры безопасного и небезопасного кода
|
||||||
|
|
||||||
|
### 📝 КАЧЕСТВО ОТВЕТОВ v10.1:
|
||||||
|
|
||||||
|
- ✅ **НОВЫЕ ЗАПРЕТЫ 27-29**: Запрет представления интерпретаций как фактов
|
||||||
|
- ✅ **ОБЯЗАТЕЛЬНЫЙ ФОРМАТ ОТВЕТОВ 17.3**: Четкое разделение фактов, интерпретаций и предположений
|
||||||
|
- ✅ **СИСТЕМА МАРКИРОВКИ**: 📖 ФАКТ, 🧠 ИНТЕРПРЕТАЦИЯ, ❓ ПРЕДПОЛОЖЕНИЕ, ⚠️ НЕ НАЙДЕНО
|
||||||
|
- ✅ **СТОП-СЛОВА**: Список категоричных утверждений для избегания без доказательств
|
||||||
|
- ✅ **ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА 11-13**: Указание источников и осторожные формулировки
|
||||||
|
@ -54,12 +54,23 @@ export function SuppliesTab() {
|
|||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
|
|
||||||
|
// Debug информация
|
||||||
|
console.log('SuppliesTab - User:', user?.phone, 'Type:', user?.organization?.type)
|
||||||
|
|
||||||
// GraphQL запросы и мутации
|
// GraphQL запросы и мутации
|
||||||
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
|
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)
|
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 || []
|
const supplies = data?.mySupplies || []
|
||||||
|
|
||||||
// Преобразуем загруженные расходники в редактируемый формат
|
// Преобразуем загруженные расходники в редактируемый формат
|
||||||
@ -187,9 +198,7 @@ export function SuppliesTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем флаги изменений
|
// Сбрасываем флаги изменений
|
||||||
setEditableSupplies((prev) =>
|
setEditableSupplies((prev) => prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })))
|
||||||
prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })),
|
|
||||||
)
|
|
||||||
|
|
||||||
toast.success('Цены успешно обновлены')
|
toast.success('Цены успешно обновлены')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -212,7 +221,9 @@ export function SuppliesTab() {
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white mb-1">Расходники со склада</h2>
|
<h2 className="text-lg font-semibold text-white mb-1">Расходники со склада</h2>
|
||||||
<p className="text-white/70 text-sm">Расходники появляются автоматически из поставок. Можно только установить цену.</p>
|
<p className="text-white/70 text-sm">
|
||||||
|
Расходники появляются автоматически из поставок. Можно только установить цену.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
@ -261,7 +272,19 @@ export function SuppliesTab() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-white mb-2">Ошибка загрузки</h3>
|
<h3 className="text-lg font-semibold text-white mb-2">Ошибка загрузки</h3>
|
||||||
<p className="text-white/70 text-sm mb-4">Не удалось загрузить расходники</p>
|
<p className="text-white/70 text-sm mb-4">
|
||||||
|
Не удалось загрузить расходники
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<span className="text-xs text-red-300">
|
||||||
|
Debug: {error.message}
|
||||||
|
<br />
|
||||||
|
User type: {user?.organization?.type}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
className="bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white"
|
className="bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white"
|
||||||
@ -301,9 +324,7 @@ export function SuppliesTab() {
|
|||||||
key={supply.id || index}
|
key={supply.id || index}
|
||||||
className={`border-t border-white/10 hover:bg-white/5 ${
|
className={`border-t border-white/10 hover:bg-white/5 ${
|
||||||
supply.hasChanges ? 'bg-blue-500/10' : ''
|
supply.hasChanges ? 'bg-blue-500/10' : ''
|
||||||
} ${
|
} ${supply.isAvailable ? '' : 'opacity-60'}`}
|
||||||
supply.isAvailable ? '' : 'opacity-60'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<td className="p-4 text-white/80">{index + 1}</td>
|
<td className="p-4 text-white/80">{index + 1}</td>
|
||||||
|
|
||||||
@ -350,9 +371,9 @@ export function SuppliesTab() {
|
|||||||
{/* Остаток на складе */}
|
{/* Остаток на складе */}
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`text-sm font-medium ${
|
<span
|
||||||
supply.isAvailable ? 'text-green-400' : 'text-red-400'
|
className={`text-sm font-medium ${supply.isAvailable ? 'text-green-400' : 'text-red-400'}`}
|
||||||
}`}>
|
>
|
||||||
{supply.warehouseStock}
|
{supply.warehouseStock}
|
||||||
</span>
|
</span>
|
||||||
{!supply.isAvailable && (
|
{!supply.isAvailable && (
|
||||||
@ -382,7 +403,9 @@ export function SuppliesTab() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-white/80">
|
<span className="text-white/80">
|
||||||
{supply.pricePerUnit ? `${parseFloat(supply.pricePerUnit).toLocaleString()} ₽` : 'Не установлена'}
|
{supply.pricePerUnit
|
||||||
|
? `${parseFloat(supply.pricePerUnit).toLocaleString()} ₽`
|
||||||
|
: 'Не установлена'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
@ -650,7 +650,6 @@ export const UPDATE_SUPPLY_PRICE = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
||||||
// Мутация для заказа поставки расходников
|
// Мутация для заказа поставки расходников
|
||||||
export const CREATE_SUPPLY_ORDER = gql`
|
export const CREATE_SUPPLY_ORDER = gql`
|
||||||
mutation CreateSupplyOrder($input: SupplyOrderInput!) {
|
mutation CreateSupplyOrder($input: SupplyOrderInput!) {
|
||||||
@ -746,6 +745,11 @@ export const CREATE_LOGISTICS = gql`
|
|||||||
description
|
description
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
fullName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -765,6 +769,11 @@ export const UPDATE_LOGISTICS = gql`
|
|||||||
description
|
description
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
fullName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -718,7 +718,7 @@ export const resolvers = {
|
|||||||
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
|
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
suppliesCount: transformedSupplies.length,
|
suppliesCount: transformedSupplies.length,
|
||||||
supplies: transformedSupplies.map(s => ({
|
supplies: transformedSupplies.map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
pricePerUnit: s.pricePerUnit,
|
pricePerUnit: s.pricePerUnit,
|
||||||
@ -826,7 +826,7 @@ export const resolvers = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Преобразуем в формат для фронтенда
|
// Преобразуем в формат для фронтенда
|
||||||
return supplies.map(supply => ({
|
return supplies.map((supply) => ({
|
||||||
...supply,
|
...supply,
|
||||||
price: supply.price ? parseFloat(supply.price.toString()) : 0,
|
price: supply.price ? parseFloat(supply.price.toString()) : 0,
|
||||||
shippedQuantity: 0, // Добавляем для совместимости
|
shippedQuantity: 0, // Добавляем для совместимости
|
||||||
@ -1387,7 +1387,6 @@ export const resolvers = {
|
|||||||
|
|
||||||
// Мои товары и расходники (для поставщиков)
|
// Мои товары и расходники (для поставщиков)
|
||||||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||||||
|
|
||||||
if (!context.user) {
|
if (!context.user) {
|
||||||
throw new GraphQLError('Требуется авторизация', {
|
throw new GraphQLError('Требуется авторизация', {
|
||||||
extensions: { code: 'UNAUTHENTICATED' },
|
extensions: { code: 'UNAUTHENTICATED' },
|
||||||
@ -1399,7 +1398,6 @@ export const resolvers = {
|
|||||||
include: { organization: true },
|
include: { organization: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if (!currentUser?.organization) {
|
if (!currentUser?.organization) {
|
||||||
throw new GraphQLError('У пользователя нет организации')
|
throw new GraphQLError('У пользователя нет организации')
|
||||||
}
|
}
|
||||||
@ -3607,7 +3605,6 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
|
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
|
||||||
updateSupplyPrice: async (
|
updateSupplyPrice: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
@ -3655,7 +3652,7 @@ export const resolvers = {
|
|||||||
const updatedSupply = await prisma.supply.update({
|
const updatedSupply = await prisma.supply.update({
|
||||||
where: { id: args.id },
|
where: { id: args.id },
|
||||||
data: {
|
data: {
|
||||||
price: args.input.pricePerUnit, // Обновляем только цену
|
pricePerUnit: args.input.pricePerUnit, // Обновляем цену продажи, НЕ цену закупки
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
include: { organization: true },
|
include: { organization: true },
|
||||||
@ -4050,7 +4047,7 @@ export const resolvers = {
|
|||||||
return {
|
return {
|
||||||
name: product.name,
|
name: product.name,
|
||||||
description: product.description || `Заказано у ${partner.name}`,
|
description: product.description || `Заказано у ${partner.name}`,
|
||||||
price: product.price,
|
price: product.price, // Цена закупки у поставщика
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
unit: 'шт',
|
unit: 'шт',
|
||||||
category: productWithCategory?.category?.name || 'Расходники',
|
category: productWithCategory?.category?.name || 'Расходники',
|
||||||
@ -5830,7 +5827,7 @@ export const resolvers = {
|
|||||||
data: {
|
data: {
|
||||||
name: item.product.name,
|
name: item.product.name,
|
||||||
description: item.product.description || `Поставка от ${existingOrder.partner.name}`,
|
description: item.product.description || `Поставка от ${existingOrder.partner.name}`,
|
||||||
price: item.price,
|
price: item.price, // Цена закупки у поставщика
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
unit: 'шт',
|
unit: 'шт',
|
||||||
category: item.product.category?.name || 'Расходники',
|
category: item.product.category?.name || 'Расходники',
|
||||||
@ -6590,7 +6587,7 @@ export const resolvers = {
|
|||||||
description: isSellerSupply
|
description: isSellerSupply
|
||||||
? `Расходники селлера ${updatedOrder.organization?.name || updatedOrder.organization?.fullName}`
|
? `Расходники селлера ${updatedOrder.organization?.name || updatedOrder.organization?.fullName}`
|
||||||
: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
|
: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
|
||||||
price: item.price,
|
price: item.price, // Цена закупки у поставщика
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
currentStock: item.quantity,
|
currentStock: item.quantity,
|
||||||
usedStock: 0,
|
usedStock: 0,
|
||||||
|
@ -528,7 +528,7 @@ export const typeDefs = gql`
|
|||||||
isAvailable: Boolean! # Есть ли на складе (влияет на цвет)
|
isAvailable: Boolean! # Есть ли на складе (влияет на цвет)
|
||||||
warehouseConsumableId: ID! # Связь со складом
|
warehouseConsumableId: ID! # Связь со складом
|
||||||
# Поля из базы данных для обратной совместимости
|
# Поля из базы данных для обратной совместимости
|
||||||
price: Float! # Из Prisma schema
|
price: Float! # Цена закупки у поставщика (не меняется)
|
||||||
quantity: Int! # Из Prisma schema
|
quantity: Int! # Из Prisma schema
|
||||||
category: String! # Из Prisma schema
|
category: String! # Из Prisma schema
|
||||||
status: String! # Из Prisma schema
|
status: String! # Из Prisma schema
|
||||||
|
Reference in New Issue
Block a user