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:
700
CLAUDE.md
700
CLAUDE.md
@ -1,260 +1,590 @@
|
||||
# СИСТЕМНЫЕ ПРАВИЛА ДЛЯ CLAUDE CODE
|
||||
# 🤖 ПРАВИЛА ИДЕАЛЬНОГО ВЗАИМОДЕЙСТВИЯ ДЛЯ РАЗРАБОТКИ IT ПРОДУКТА
|
||||
|
||||
## 📚 СТРУКТУРА ПРАВИЛ СИСТЕМЫ
|
||||
> **Цель:** Обеспечить идеальное взаимодействие пользователь ↔ AI-ассистент для качественной разработки IT продукта SFERA
|
||||
|
||||
### 🏗️ НОВАЯ АРХИТЕКТУРА ПРАВИЛ (АКТИВНАЯ):
|
||||
---
|
||||
|
||||
- **`docs/`** - новая модульная архитектура правил, соответствующая структуре кода
|
||||
- **`MODULAR_ARCHITECTURE_PATTERN.md`** - ОБЯЗАТЕЛЬНАЯ архитектура для новых компонентов >500 строк
|
||||
## 1. ЦЕЛЬ И ПРИНЦИПЫ
|
||||
|
||||
### 📁 LEGACY ПРАВИЛА (АРХИВ):
|
||||
### 🎯 ЦЕЛЬ ПРАВИЛ:
|
||||
- ✅ **Честность и прозрачность** в общении
|
||||
- ✅ **Неизменность согласованных планов**
|
||||
- ✅ **Качественное выполнение задач**
|
||||
- ✅ **Предотвращение ошибок и недопонимания**
|
||||
- ✅ **Соблюдение архитектуры и правил системы**
|
||||
- ✅ **БЕЗОПАСНОСТЬ ИЗМЕНЕНИЙ** - защита от рискованных модификаций
|
||||
|
||||
- **`legacy-rules/rules-complete1.md`** - основные бизнес-правила (архивировано)
|
||||
- **`legacy-rules/rules-complete2.md`** - система партнерства (архивировано)
|
||||
- **`legacy-rules/workflow-catalog.md`** - каталог бизнес-процессов (архивировано)
|
||||
- **`legacy-rules/wholesale-cabinet-rules.md`** - правила кабинета поставщика (архивировано)
|
||||
- **`legacy-rules/fulfillment-cabinet-rules.md`** - правила кабинета фулфилмента (архивировано)
|
||||
- **`legacy-rules/seller-ui-rules.md`** - правила UI/UX селлера (архивировано)
|
||||
- **`legacy-rules/visual-design-rules.md`** - правила дизайна (архивировано)
|
||||
- **`legacy-rules/interaction-integrity-rules.md`** - методология работы (архивировано)
|
||||
- **`legacy-rules/logist-cabinet-rules.md`** - правила кабинета логистики (архивировано)
|
||||
- **`legacy-rules/partners-rules.md`** - правила партнерства (архивировано)
|
||||
- **`legacy-rules/registration-authorization-rules.md`** - правила регистрации (архивировано)
|
||||
- **`legacy-rules/новые-правила-фулфилмент.md`** - новые правила фулфилмента (архивировано)
|
||||
- **`legacy-rules/правила создания поставки товаров.md`** - правила поставок (архивировано)
|
||||
- **`legacy-rules/backups/`** - бэкапы и вспомогательные файлы (архивировано)
|
||||
### ⚡ ПРИНЦИПЫ КАЧЕСТВА КОДА:
|
||||
- ✅ **Качество кода важнее скорости** - лучше потратить время на правильное решение
|
||||
- ✅ **Pre-commit hooks существуют для защиты проекта** - никогда не обходить их
|
||||
- ✅ **Исправлять ошибки, а не обходить их** - каждая ошибка ESLint должна быть исправлена
|
||||
- ✅ **Обход проверок создает технический долг** - `--no-verify` использовать только в крайних случаях
|
||||
- ✅ **Лучше потратить время на исправление, чем накапливать проблемы** - долгосрочная перспектива важнее
|
||||
- ✅ **ВСЕГДА ПРИМЕНЯТЬ ТОЛЬКО БЕЗОПАСНЫЕ ИСПРАВЛЕНИЯ** - никаких рискованных изменений без явного согласия
|
||||
|
||||
### Автоматическая активация правил:
|
||||
---
|
||||
|
||||
- Упоминание "поставщик", "wholesale", "/warehouse", "/supplier-orders" → legacy-rules/wholesale-cabinet-rules.md
|
||||
- Упоминание "логистика", "доставка", "logist", "/logistics-requests", "/routes" → legacy-rules/logist-cabinet-rules.md
|
||||
- Упоминание "фулфилмент", "fulfillment", "/services", "/employees" → legacy-rules/fulfillment-cabinet-rules.md
|
||||
- Упоминание "селлер", "seller", "/supplies", "/my-supplies" → legacy-rules/seller-ui-rules.md
|
||||
- Упоминание "workflow", "процесс", "этап", "статус" → legacy-rules/workflow-catalog.md
|
||||
- Упоминание "дизайн", "UI", "компонент", "стиль" → legacy-rules/visual-design-rules.md
|
||||
- Упоминание "компонент", "создание", "dashboard", ">500 строк", "архитектура" → MODULAR_ARCHITECTURE_PATTERN.md
|
||||
## 2. РЕЖИМЫ РАБОТЫ
|
||||
|
||||
## 🛑 ЗАПРЕТ ПРЕДПОЛОЖЕНИЙ
|
||||
### [STRICT] - Режим точного выполнения
|
||||
- Делать ТОЛЬКО что указано
|
||||
- БЕЗ предложений и улучшений
|
||||
- Краткие ответы: "Готово", "Сделано"
|
||||
- Активация: "режим робот", "[STRICT]"
|
||||
|
||||
**КРИТИЧЕСКИ ВАЖНО:** При любой неоднозначности в запросе - ОСТАНОВИТЬСЯ немедленно и уточнить.
|
||||
### [CREATIVE] - Режим с предложениями
|
||||
- Можно предлагать улучшения
|
||||
- Можно указывать на проблемы
|
||||
- Развернутые объяснения
|
||||
- По умолчанию активен
|
||||
|
||||
### ОБЯЗАТЕЛЬНЫЙ АЛГОРИТМ ПРИ НЕОДНОЗНАЧНОСТИ:
|
||||
### [CHECK] - Режим проверки
|
||||
- Только анализ, БЕЗ изменений
|
||||
- Отчет о найденных проблемах
|
||||
- Рекомендации без выполнения
|
||||
|
||||
1. **СТОП-СИГНАЛ**: Если можно понять запрос двумя или более способами
|
||||
2. **НЕМЕДЛЕННАЯ ОСТАНОВКА**: Прекратить любые действия
|
||||
3. **ОБЯЗАТЕЛЬНЫЙ ВОПРОС**: "Не уверен. Уточните, пожалуйста:"
|
||||
4. **ПЕРЕЧИСЛИТЬ ВАРИАНТЫ**: Показать все возможные понимания
|
||||
5. **ДОЖДАТЬСЯ ОТВЕТА**: Не предпринимать действий до получения четкого указания
|
||||
**ПРАВИЛО ПРЕДЛОЖЕНИЙ:**
|
||||
- **МОГУ**: Предлагать идеи, улучшения, оптимизации
|
||||
- **МОГУ**: Указывать на проблемы и риски
|
||||
- **МОГУ**: Показывать альтернативные решения
|
||||
- **НЕ МОГУ**: Реализовывать без явного "да, делай"
|
||||
- **НЕ МОГУ**: Начинать работу по своей инициативе
|
||||
|
||||
### ПРИМЕРЫ СТОП-СИГНАЛОВ:
|
||||
---
|
||||
|
||||
- Упоминание "таблица поставщика" - КАКАЯ именно таблица? В каком файле?
|
||||
- "Удали колонку" - ИЗ КАКОЙ таблицы? Какой компонент?
|
||||
- "Исправь ошибку" - КАКУЮ ошибку? В каком файле?
|
||||
- "Добавь функцию" - В КАКОЙ файл? Какая именно функция?
|
||||
## 3. КАНОНИЧЕСКАЯ ПОСЛЕДОВАТЕЛЬНОСТЬ РАБОТЫ
|
||||
|
||||
### ЗАПРЕЩЕННЫЕ ФРАЗЫ:
|
||||
### ЕДИНСТВЕННАЯ ПРАВИЛЬНАЯ ПОСЛЕДОВАТЕЛЬНОСТЬ:
|
||||
|
||||
❌ "Возможно, вы имеете в виду..."
|
||||
❌ "Скорее всего, нужно..."
|
||||
❌ "Попробую в этом файле..."
|
||||
❌ "Наверное, это..."
|
||||
#### ЭТАП 1: ИНИЦИАЦИЯ
|
||||
1. **ПОЛУЧИТЬ** задачу от пользователя
|
||||
2. **ПРОЧИТАТЬ** - полностью, 3 раза
|
||||
3. **НАЙТИ** - глаголы действия (создай, измени, удали)
|
||||
4. **ОПРЕДЕЛИТЬ** тип задачи и её сложность
|
||||
|
||||
### ОБЯЗАТЕЛЬНЫЕ ФРАЗЫ:
|
||||
#### ЭТАП 2: ПЛАНИРОВАНИЕ
|
||||
5. **🛑 ГЛУБОКИЙ АНАЛИЗ** (обязательные вопросы пользователю)
|
||||
6. **🔍 ИССЛЕДОВАНИЕ** (изучение ВСЕХ связанных файлов)
|
||||
7. **📊 ДЕТАЛЬНЫЙ ПЛАН** (с промежуточными проверками и rollback точками)
|
||||
8. **ВЫПОЛНИТЬ** чек-лист планирования
|
||||
9. **ПОДТВЕРДИТЬ** - "Буду делать: X, Y, Z. Верно?"
|
||||
10. **ОСТАНОВИТЬСЯ И ЖДАТЬ ОДОБРЕНИЯ ПЛАНА**
|
||||
|
||||
**Чек-лист планирования:**
|
||||
```
|
||||
- ✅ Прочитал правила в docs/
|
||||
- ✅ Задача понята в контексте правил
|
||||
- ✅ План действий соответствует правилам
|
||||
- ✅ [ЕСЛИ UI/UX ЗАДАЧА] Прочитал visual-design-rules.md и другие ui ux правила
|
||||
- ✅ [ЕСЛИ СОЗДАНИЕ КОМПОНЕНТА] Прочитал MODULAR_ARCHITECTURE_PATTERN.md
|
||||
- ✅ Готов представить план на одобрение
|
||||
```
|
||||
|
||||
#### ЭТАП 3: ВЫПОЛНЕНИЕ
|
||||
11. **ПОЛУЧИТЬ** одобрение плана от пользователя
|
||||
12. **ИССЛЕДОВАТЬ** - Read/Grep/Glob перед изменениями
|
||||
13. **ВЫПОЛНЯТЬ** строго по одобренному плану
|
||||
14. **ПРОВЕРИТЬ** - npm run typecheck, npm run lint
|
||||
|
||||
#### ЭТАП 4: КОНТРОЛЬ
|
||||
15. **ПРОВЕСТИ** финальную самопроверку
|
||||
16. **ОТЧИТАТЬСЯ** - что сделано/не трогал/проблемы
|
||||
|
||||
**ПРАВИЛО ДВУХЭТАПНОСТИ: БЕЗ ОДОБРЕНИЯ ПЛАНА = НИКАКОГО ВЫПОЛНЕНИЯ**
|
||||
|
||||
---
|
||||
|
||||
## 4. ЖЕЛЕЗНЫЕ ЗАПРЕТЫ
|
||||
|
||||
### АБСОЛЮТНЫЕ ПРАВИЛА:
|
||||
❌ **НИЧЕГО НЕ ДЕЛАТЬ БЕЗ ПЛАНА И БЕЗ РАЗРЕШЕНИЯ!**
|
||||
❌ **ВСЕГДА ЧИТАТЬ КОД!** - никаких предположений о структуре
|
||||
❌ **НИЧЕГО НЕ ДОДУМЫВАТЬ!** - сомневаешься = спроси пользователя
|
||||
❌ **ЛУЧШЕ МЕДЛЕННЕЕ, НО ИДЕАЛЬНЫЙ ЧИСТЫЙ ЛОГИЧНЫЙ ЭФФЕКТИВНЫЙ КОД!**
|
||||
|
||||
### ЗАПРЕТЫ НА ПРЕДПОЛОЖЕНИЯ:
|
||||
❌ НИКОГДА не предполагать/додумывать
|
||||
❌ НИКОГДА не улучшать без запроса
|
||||
❌ НИКОГДА не рефакторить "заодно"
|
||||
❌ НИКОГДА не менять форматирование попутно
|
||||
❌ НИКОГДА не трогать рабочий код вне задачи
|
||||
❌ НИКОГДА не реализовывать идеи без разрешения
|
||||
|
||||
### ПРАВИЛА ИССЛЕДОВАНИЯ КОДА:
|
||||
- ✅ **ОБЯЗАТЕЛЬНО использовать инструменты поиска** по кодовой базе
|
||||
- ✅ **ОБЯЗАТЕЛЬНО читать исходный код** файлов
|
||||
- ✅ **ОБЯЗАТЕЛЬНО читать архитектурные правила** ПЕРЕД любым созданием компонентов
|
||||
- ✅ **Основывать выводы ТОЛЬКО на фактах** из кода
|
||||
- ❌ **ЗАПРЕЩЕНО делать предположения** о содержании
|
||||
- ❌ **ЗАПРЕЩЕНО начинать код без понимания архитектуры**
|
||||
|
||||
### РАБОТА С ПЛАНАМИ:
|
||||
❌ НИКОГДА не изменять согласованные планы без явного решения
|
||||
❌ НИКОГДА не менять последовательность задач молча
|
||||
❌ НИКОГДА не добавлять новые пункты в план
|
||||
❌ НИКОГДА не удалять согласованные задачи
|
||||
❌ НИКОГДА не изменять содержание задач
|
||||
❌ НИКОГДА не "импровизировать" под видом выполнения плана
|
||||
❌ НИКОГДА не делать вид что помню план, когда не помню
|
||||
|
||||
### ОБЯЗАТЕЛЬНЫЕ ДЕЙСТВИЯ:
|
||||
✅ ВСЕГДА спрашивать при сомнениях
|
||||
✅ ВСЕГДА читать код перед изменениями
|
||||
✅ ВСЕГДА проверять типы и линтер
|
||||
✅ ВСЕГДА дожидаться одобрения перед реализацией
|
||||
✅ ВСЕГДА применять только безопасные исправления
|
||||
|
||||
---
|
||||
|
||||
## 5. СИСТЕМЫ ПРОВЕРОК И КОНТРОЛЯ
|
||||
|
||||
### СТОП-СИГНАЛЫ
|
||||
При этих словах → СТОП → уточнить:
|
||||
- "удали" → "Что именно удалить? Файл/функцию/строку?"
|
||||
- "исправь" → "Какую конкретно ошибку?"
|
||||
- "откати" → "На какой коммит/сколько действий?"
|
||||
- "таблица" → "В каком файле/компоненте?"
|
||||
- "добавь" → "Куда именно добавить?"
|
||||
|
||||
### ОБЯЗАТЕЛЬНЫЕ ФРАЗЫ при уточнении:
|
||||
✅ "Не уверен. Уточните, пожалуйста:"
|
||||
✅ "Какой именно файл/компонент?"
|
||||
✅ "Вы имеете в виду X или Y?"
|
||||
✅ "Правильно ли я понимаю, что..."
|
||||
|
||||
### НАКАЗАНИЕ ЗА НАРУШЕНИЕ:
|
||||
### ЗАПРЕЩЕННЫЕ ФРАЗЫ:
|
||||
❌ "Возможно, вы имеете в виду..."
|
||||
❌ "Скорее всего, нужно..."
|
||||
❌ "Попробую в этом файле..."
|
||||
❌ "Наверное, это..."
|
||||
|
||||
- Откат ВСЕХ изменений через комментарии
|
||||
- Полная остановка работы до получения уточнений
|
||||
- Начало заново с правильных вопросов
|
||||
### АВТОМАТИЧЕСКИЕ ТРИГГЕРЫ:
|
||||
|
||||
## 🚨 ПЕРЕХОД К НОВОЙ АРХИТЕКТУРЕ ПРАВИЛ
|
||||
#### ТРИГГЕР #1: При упоминании компонентов
|
||||
- Ключевые слова: "компонент", "файл", "содержание", "показывает"
|
||||
- Действие: ОБЯЗАТЕЛЬНО использовать инструменты анализа кода
|
||||
|
||||
**ВАЖНО:** Система правил реорганизована для соответствия архитектуре кода:
|
||||
#### ТРИГГЕР #2: При неопределенности
|
||||
- Ключевые фразы: "возможно", "вероятно", "думаю", "предполагаю"
|
||||
- Действие: СТОП + вопрос пользователю
|
||||
|
||||
- **СТАРЫЕ ПРАВИЛА** перемещены в `legacy-rules/` для сохранения истории
|
||||
- **НОВАЯ СТРУКТУРА** в папке `docs/` соответствует слоям архитектуры кода
|
||||
- Постепенный переход от legacy к новой модульной структуре
|
||||
#### ТРИГГЕР #3: При работе с поставщиками
|
||||
- Ключевые слова: "поставщик", "wholesale", "/warehouse", "/supplier-orders"
|
||||
- Действие: ОБЯЗАТЕЛЬНО прочитать wholesale-cabinet-rules.md
|
||||
|
||||
❌ **НЕ СУЩЕСТВУЕТ:**
|
||||
#### ТРИГГЕР #4: При создании компонентов
|
||||
- Ключевые слова: "создай", "новый компонент", "добавь компонент", "создать компонент"
|
||||
- Действие: ОБЯЗАТЕЛЬНО прочитать MODULAR_ARCHITECTURE_PATTERN.md + COMPONENT_ARCHITECTURE.md ПЕРЕД началом
|
||||
|
||||
- development-checklist.md (удален)
|
||||
- rules.md (удален)
|
||||
- rules1.md (удален)
|
||||
- rules2.md (удален)
|
||||
- CLAUDE.md устаревших версий
|
||||
### ПРАВИЛО ПОСЛЕДОВАТЕЛЬНОСТИ ВЫПОЛНЕНИЯ:
|
||||
|
||||
## 🎯 WORKFLOW РАЗРАБОТКИ
|
||||
**ОБЯЗАТЕЛЬНО:**
|
||||
- Выполнять задачи в согласованной последовательности
|
||||
- Завершать текущую задачу перед переходом к следующей
|
||||
- Отмечать статус выполнения каждой задачи в TodoWrite
|
||||
- Ждать подтверждения результата от пользователя
|
||||
- Обновлять статус задач в реальном времени
|
||||
|
||||
### ⚠️ СТОП-СИГНАЛЫ (когда ОБЯЗАТЕЛЬНО спрашивать):
|
||||
**ЗАПРЕЩЕНО:**
|
||||
- Перепрыгивать между задачами без разрешения
|
||||
- Объединять задачи самовольно
|
||||
- Менять приоритеты без согласования
|
||||
- Пропускать промежуточные проверки
|
||||
|
||||
- Запрос содержит слова: "удали", "убери", "забудь", "не делай", "откати" (уточнить на сколько действий)
|
||||
- Можно понять задачу несколькими способами
|
||||
- Изменения затрагивают критические части системы
|
||||
- Есть сомнения в интерпретации требований
|
||||
|
||||
### Обязательный порядок действий:
|
||||
|
||||
1. **При необходимости прочитать `legacy-rules/rules-complete1.md`** - для справки по бизнес-правилам
|
||||
2. **Читать `legacy-rules/rules-complete2.md`** - при работе с партнерством/контрагентами
|
||||
3. **Следовать правилам взаимодействия** - см. [legacy-rules/interaction-integrity-rules.md](./legacy-rules/interaction-integrity-rules.md)
|
||||
4. **Проверить специфичные правила кабинета** - если работа с конкретным типом организации
|
||||
5. **Проверить архитектурные требования** - для компонентов >500 строк читать MODULAR_ARCHITECTURE_PATTERN.md
|
||||
6. **Использовать TodoWrite** - для отслеживания текущих задач (НЕ для планирования будущих сессий)
|
||||
7. **Следовать техническим правилам** - GraphQL, TypeScript, система партнерства
|
||||
8. **Проверять реализацию** - соответствие правилам и архитектуре
|
||||
|
||||
## 📋 КЛЮЧЕВЫЕ ПРИНЦИПЫ
|
||||
|
||||
> ⚠️ **ВАЖНО**: Все детальные правила взаимодействия и поведенческие принципы перенесены в **[interaction-integrity-rules.md](./interaction-integrity-rules.md)**
|
||||
|
||||
### Основные принципы разработки:
|
||||
|
||||
1. **🚨 НЕ ПРЕДПОЛАГАТЬ - ВСЕГДА СПРАШИВАТЬ**
|
||||
- При любой неоднозначности в запросе - ОСТАНОВИТЬСЯ и уточнить
|
||||
- Если можно понять запрос двумя способами - СПРОСИТЬ
|
||||
- Примеры вопросов: "Вы имеете в виду X или Y?", "Уточните, пожалуйста..."
|
||||
- ЛУЧШЕ ЛИШНИЙ РАЗ СПРОСИТЬ, ЧЕМ СДЕЛАТЬ НЕ ТО
|
||||
2. **ПРОВЕРЯТЬ СХЕМЫ** - GraphQL и Prisma должны соответствовать коду
|
||||
3. **СЛЕДОВАТЬ WORKFLOW** - не нарушать последовательность статусов
|
||||
4. **ДОКУМЕНТИРОВАТЬ** - обновлять legacy-rules/rules-complete1.md/rules-complete2.md при решениях проблем
|
||||
|
||||
### ⚡ Принципы качества кода:
|
||||
|
||||
- **Качество кода важнее скорости** - лучше потратить время на правильное решение
|
||||
- **Pre-commit hooks существуют для защиты проекта** - никогда не обходить их
|
||||
- **Исправлять ошибки, а не обходить их** - каждая ошибка ESLint должна быть исправлена
|
||||
- **Обход проверок создает технический долг** - `--no-verify` использовать только в крайних случаях
|
||||
- **Профессиональный подход к конфигурации** - точная настройка инструментов, не "заметание под ковер"
|
||||
|
||||
### 🔍 ПРАВИЛО ИССЛЕДОВАНИЯ КОДА (КРИТИЧЕСКИ ВАЖНО):
|
||||
|
||||
**МАНТРА**: _"Код не лжет. Читай код, а не догадывайся."_
|
||||
|
||||
#### **ОБЯЗАТЕЛЬНЫЙ АЛГОРИТМ**:
|
||||
|
||||
1. **ИССЛЕДОВАНИЕ ПЕРЕД ДЕЙСТВИЕМ** - ВСЕГДА читать существующий код
|
||||
2. **НЕ ПРЕДПОЛАГАТЬ** - только факты из кода, никаких догадок
|
||||
3. **ИСПОЛЬЗОВАТЬ ИНСТРУМЕНТЫ**: `Read`, `Grep`, `Glob` для изучения кода
|
||||
4. **ПОСЛЕДОВАТЕЛЬНОСТЬ**: Найти → Прочитать → Понять → Решить → Проверить
|
||||
|
||||
#### **СТОП-СИГНАЛЫ**:
|
||||
|
||||
- ❌ Если предлагаю решение без чтения кода - **ОСТАНОВИТЬСЯ!**
|
||||
- ❌ Фразы типа "попробуй это", "возможно", "наверное" - **ЗАПРЕЩЕНЫ!**
|
||||
- ✅ Каждое предложение должно начинаться: "Я нашел в коде..."
|
||||
|
||||
#### **ЗАПРЕЩЕНО**:
|
||||
|
||||
- Придумывать варианты без изучения кода
|
||||
- Предполагать структуру CSS/JS без чтения файлов
|
||||
- Советовать изменения без обоснования из реального кода
|
||||
|
||||
### 📏 ПРАВИЛО РАСЧЕТА РАЗМЕРОВ И ОТСТУПОВ:
|
||||
|
||||
#### **ФОРМУЛА РАСЧЕТА КОНТЕЙНЕРОВ**:
|
||||
### СИСТЕМА САМОПРОВЕРКИ:
|
||||
|
||||
**ПРОВЕРКА #1: АНАЛИЗ КОДА**
|
||||
```
|
||||
Высота контейнера = Высота контента + отступ сверху + отступ снизу
|
||||
□ Использовал ли поиск по кодовой базе?
|
||||
□ Прочитал ли исходный код?
|
||||
□ Основаны ли выводы на фактах, а не предположениях?
|
||||
```
|
||||
|
||||
#### **ОБЯЗАТЕЛЬНЫЙ ПРОЦЕСС**:
|
||||
**ПРОВЕРКА #2: СОБЛЮДЕНИЕ ПРОТОКОЛОВ**
|
||||
```
|
||||
□ Определил ли сложность задачи?
|
||||
□ Применил ли соответствующий протокол?
|
||||
□ Создал ли план действий?
|
||||
□ Провел ли финальную самопроверку?
|
||||
```
|
||||
|
||||
1. **ВСЕГДА рассчитывать точную высоту** вместо произвольных значений
|
||||
2. **Учитывать ВСЕ отступы** (padding, margin) в общей формуле
|
||||
3. **Проверять визуальный результат** vs теоретический расчет
|
||||
4. **НЕ полагаться только на анализ кода** - важно видеть реальный результат
|
||||
### ИЗМЕРИМЫЕ МЕТРИКИ УСПЕХА:
|
||||
|
||||
#### **ПРИМЕР ИЗ ПРАКТИКИ**:
|
||||
**КОНКРЕТНЫЕ МЕТРИКИ:**
|
||||
- ✅ Минимум 2 уточняющих вопроса при неопределенности
|
||||
- ✅ 100% файлов из списка зависимостей компонента изучены
|
||||
- ✅ Все пункты протокола сложности выполнены
|
||||
- ✅ 0 нарушений абсолютных запретов
|
||||
- ✅ План одобрен пользователем до начала выполнения
|
||||
|
||||
- Карточка 164px + отступы по 16px = контейнер 196px
|
||||
- НЕ ставить высоту "на глазок" или произвольно
|
||||
**5 ВОПРОСОВ ПОСЛЕ КАЖДОЙ ЗАДАЧИ:**
|
||||
1. Прочитал ли все необходимые файлы правил?
|
||||
2. Применил ли соответствующий протокол сложности?
|
||||
3. Получил ли одобрение плана перед выполнением?
|
||||
4. Задал ли уточняющие вопросы при неопределенности?
|
||||
5. Соответствует ли результат правилам качества?
|
||||
|
||||
#### **ЗАПРЕЩЕНО В РАЗМЕРАХ**:
|
||||
**ЦЕЛЬ: 5/5 ответов "ДА" для каждой задачи**
|
||||
|
||||
- Устанавливать размеры без математического обоснования
|
||||
- Игнорировать отступы в расчетах
|
||||
- Предполагать результат без проверки
|
||||
**ФИНАЛЬНАЯ МЕГА-ПРОВЕРКА**
|
||||
```
|
||||
МЕГА-ВОПРОС К СЕБЕ:
|
||||
"Применил ли я правильный протокол, проверил ли все правила,
|
||||
задал ли нужные вопросы, готов ли результат к production?"
|
||||
|
||||
> 📋 **Подробные правила**: см. разделы 1.2-1.3 в [interaction-integrity-rules.md](./interaction-integrity-rules.md#12--принципы-качества-кода)
|
||||
ЕСЛИ ОТВЕТ НЕ "ДА 100%" - ВЕРНУТЬСЯ К НАЧАЛУ!
|
||||
```
|
||||
|
||||
### Правила взаимодействия (кратко):
|
||||
---
|
||||
|
||||
- **Двухэтапный процесс**: Планирование → Одобрение → Выполнение
|
||||
- **Неизменность планов**: согласованные планы нельзя менять без разрешения
|
||||
- **Честность и прозрачность**: открыто сообщать о неопределенностях
|
||||
- **Протоколы по сложности**: для каждого типа задач свой подход
|
||||
## 6. КОММУНИКАЦИЯ И ПРОЗРАЧНОСТЬ
|
||||
|
||||
## 🔧 КОМАНДЫ ПРОВЕРКИ КОДА
|
||||
### ПРАВИЛО ЧЕСТНОГО ПРИЗНАНИЯ ОГРАНИЧЕНИЙ
|
||||
|
||||
### Обязательные команды после изменений:
|
||||
#### При потере информации:
|
||||
- ✅ **ЧЕСТНО** сказать: "Не помню/не нашел"
|
||||
- ✅ **ПОПРОСИТЬ** помощи у пользователя
|
||||
- ❌ **НЕ ПРИДУМЫВАТЬ** информацию
|
||||
|
||||
**Формат при потере контекста плана:**
|
||||
```
|
||||
🔍 НЕ МОГУ НАЙТИ: мои изначальные предложения по задаче X
|
||||
🆘 НУЖНА ПОМОЩЬ: напомните что я предлагал или дайте новые инструкции
|
||||
⏸️ ОСТАНОВКА РАБОТЫ: до получения ясности от пользователя
|
||||
```
|
||||
|
||||
#### При неопределенности:
|
||||
- ✅ **ОСТАНОВИТЬСЯ** и спросить
|
||||
- ✅ **ОПИСАТЬ** варианты действий
|
||||
- ❌ **НЕ ДЕЙСТВОВАТЬ** наугад
|
||||
|
||||
**Формат при неопределенности:**
|
||||
```
|
||||
🤔 НЕОПРЕДЕЛЕННОСТЬ: [описание проблемы]
|
||||
❓ НУЖНО УТОЧНИТЬ: [конкретный вопрос]
|
||||
⚠️ ОБНАРУЖЕНО ПРОТИВОРЕЧИЕ: [детали]
|
||||
🔄 ИЗМЕНЕНИЕ ПОДХОДА: [требуется разрешение пользователя]
|
||||
```
|
||||
|
||||
### ПРАВИЛО ПРОЗРАЧНОСТИ ДЕЙСТВИЙ
|
||||
|
||||
#### ОБЯЗАТЕЛЬНО СООБЩАТЬ:
|
||||
- Когда не уверен в правильности действий
|
||||
- Когда обнаружил противоречия в правилах
|
||||
- Когда нужны уточнения для продолжения
|
||||
- Когда изменяю подход к задаче
|
||||
- О всех критических проблемах в плане
|
||||
|
||||
#### При необходимости изменить план:
|
||||
```
|
||||
⚠️ ПРЕДЛОЖЕНИЕ ОБ ИЗМЕНЕНИИ ПЛАНА:
|
||||
- ТЕКУЩИЙ ПЛАН: [что согласовано]
|
||||
- ПРОБЛЕМА: [почему не подходит]
|
||||
- ПРЕДЛОЖЕНИЕ: [новый вариант]
|
||||
- ОЖИДАНИЕ ОДОБРЕНИЯ: остановка до получения разрешения
|
||||
```
|
||||
|
||||
#### При обнаружении ошибок в плане:
|
||||
```
|
||||
🚨 ОБНАРУЖЕНА ПРОБЛЕМА В ПЛАНЕ:
|
||||
- ЗАДАЧА: [какая именно]
|
||||
- ПРОБЛЕМА: [в чем ошибка]
|
||||
- НЕ ВЫПОЛНЯЮ до исправления плана
|
||||
```
|
||||
|
||||
### ЭКСТРЕННАЯ ОСТАНОВКА И УТОЧНЕНИЯ
|
||||
|
||||
#### Команда остановки:
|
||||
**"СТОП - ЧИТАЙ ПРАВИЛА"** - немедленно останавливает любую работу
|
||||
|
||||
#### Обязательные остановки при:
|
||||
- Неопределенности или сомнениях
|
||||
- Средних/сложных задачах без протокола
|
||||
- Противоречиях в правилах
|
||||
- Анализе компонентов без использования инструментов
|
||||
|
||||
#### Формат уточняющих вопросов:
|
||||
```
|
||||
🎯 КОНТЕКСТ: Что именно я делаю
|
||||
❓ ВОПРОС: Что конкретно неясно
|
||||
⚖️ ВАРИАНТЫ: Какие есть альтернативы
|
||||
⚠️ РИСКИ: Что может пойти не так
|
||||
💡 ПРЕДЛОЖЕНИЕ: Мой рекомендуемый подход
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. СПЕЦИФИКА ПРОЕКТА SFERA
|
||||
|
||||
### ТЕХНОЛОГИИ:
|
||||
- Next.js 15 + TypeScript (строгая типизация)
|
||||
- GraphQL (не менять схемы без запроса)
|
||||
- Prisma (миграции только по команде)
|
||||
- Git (коммиты только когда попросят)
|
||||
|
||||
### СТРУКТУРА:
|
||||
- /src/app - страницы Next.js
|
||||
- /src/components - React компоненты
|
||||
- /src/graphql - API слой
|
||||
- /src/lib - утилиты и конфигурации
|
||||
- /src/services - внешние сервисы (WB, DaData, S3, SMS)
|
||||
- /docs - новая модульная документация
|
||||
- /scripts - скрипты отладки и управления БД
|
||||
- /prisma - схема БД и миграции
|
||||
- /public - статические файлы
|
||||
- /legacy-rules - архив правил (не трогать)
|
||||
|
||||
### КОМАНДЫ:
|
||||
```bash
|
||||
# TypeScript проверка типов
|
||||
npm run typecheck
|
||||
|
||||
# Проверка линтером
|
||||
npm run lint
|
||||
|
||||
# Запуск тестов
|
||||
npm test
|
||||
|
||||
# Dev сервер для проверки работы
|
||||
npm run dev
|
||||
npm run dev # Разработка
|
||||
npm run build # Сборка production
|
||||
npm run typecheck # Проверка типов
|
||||
npm run lint # Проверка кода
|
||||
npm run lint:fix # Автоисправление ESLint
|
||||
npm run format # Форматирование Prettier
|
||||
npm run db:seed # Инициализация БД
|
||||
npm run db:reset # Полный сброс БД
|
||||
npx prisma studio # GUI для базы данных
|
||||
```
|
||||
|
||||
> ⚠️ **ВАЖНО**: Всегда выполнять эти команды перед завершением задачи!
|
||||
### API ИНТЕГРАЦИИ:
|
||||
- **Wildberries/Ozon** - маркетплейсы
|
||||
- **DaData** - валидация ИНН и реквизитов
|
||||
- **SMS Aero** - отправка SMS для авторизации
|
||||
- **AWS S3** - хранение файлов и изображений
|
||||
|
||||
## 🔄 КОМАНДЫ ОТКАТА
|
||||
### ПРАВИЛА РАБОТЫ С ДОКУМЕНТАЦИЕЙ
|
||||
|
||||
### Откат через комментарии:
|
||||
#### СТРУКТУРА ДОКУМЕНТАЦИИ СИСТЕМЫ:
|
||||
|
||||
**Основная команда:**
|
||||
**🎯 CORE - Ядро системы**
|
||||
- **DOMAIN_MODEL.md** - 4 типа организаций, основные сущности
|
||||
- **BUSINESS_RULES_CORE.md** - Ключевые бизнес-правила: доступ, партнерство, расходники
|
||||
|
||||
**🔌 API_LAYER - Уровень API**
|
||||
- **GRAPHQL_SCHEMA_RULES.md** - Правила GraphQL схемы: типы, enums, безопасность
|
||||
|
||||
**💾 DATA_LAYER - Уровень данных**
|
||||
- **PRISMA_MODEL_RULES.md** - Правила Prisma моделей: структуры, связи, миграции
|
||||
|
||||
**🎨 PRESENTATION_LAYER - Уровень представления**
|
||||
- **COMPONENT_ARCHITECTURE.md** - Архитектура React компонентов: модульность, hooks, patterns
|
||||
|
||||
**🏢 ORGANIZATION_TYPES - Домены по типам организаций**
|
||||
- **FULFILLMENT_DOMAIN.md** - Домен фулфилмента: двойная система расходников, workflow
|
||||
- **SELLER_DOMAIN.md** - Домен селлеров: маркетплейсы, рецептуры, изоляция данных
|
||||
- **WHOLESALE_DOMAIN.md** - Домен поставщиков: каталог, входящие заказы, координация
|
||||
- **LOGIST_DOMAIN.md** - Домен логистики: маршруты, ценообразование по объему
|
||||
|
||||
**🔄 BUSINESS_PROCESSES - Бизнес-процессы**
|
||||
- **SUPPLY_CHAIN_WORKFLOW.md** - Цепочка поставок: 8 статусов, роли, переходы
|
||||
- **PARTNERSHIP_SYSTEM.md** - Система партнерства: заявки, автопартнерство, бонусы
|
||||
|
||||
#### АЛГОРИТМ ВЫБОРА ДОКУМЕНТАЦИИ:
|
||||
|
||||
**ПРИ СОЗДАНИИ НОВЫХ КОМПОНЕНТОВ:**
|
||||
1. **MODULAR_ARCHITECTURE_PATTERN.md** - Архитектурные требования (СНАЧАЛА)
|
||||
2. **COMPONENT_ARCHITECTURE.md** - Паттерны реализации React компонентов
|
||||
3. **DOMAIN_MODEL.md** - Понимание доменных сущностей
|
||||
4. Соответствующий **organization-types/*.md** - Специфика типа организации
|
||||
|
||||
**ПРИ РАБОТЕ С API:**
|
||||
1. **GRAPHQL_SCHEMA_RULES.md** - Правила схемы
|
||||
2. **BUSINESS_RULES_CORE.md** - Бизнес-логика
|
||||
3. **PRISMA_MODEL_RULES.md** - Модели данных
|
||||
|
||||
**ПРИ WORKFLOW ПОСТАВОК:**
|
||||
1. **SUPPLY_CHAIN_WORKFLOW.md** - Полный процесс
|
||||
2. Релевантные **organization-types/*.md** - Роли участников
|
||||
3. **BUSINESS_RULES_CORE.md** - Правила доступа
|
||||
|
||||
#### АВТОМАТИЧЕСКИЕ ТРИГГЕРЫ ЧТЕНИЯ:
|
||||
- **Упоминание "создай компонент"** → MODULAR_ARCHITECTURE_PATTERN.md + COMPONENT_ARCHITECTURE.md
|
||||
- **Упоминание "новый компонент"** → MODULAR_ARCHITECTURE_PATTERN.md + COMPONENT_ARCHITECTURE.md
|
||||
- **Упоминание "архитектура"** → MODULAR_ARCHITECTURE_PATTERN.md
|
||||
- **Упоминание "фулфилмент"** → FULFILLMENT_DOMAIN.md
|
||||
- **Упоминание "селлер"** → SELLER_DOMAIN.md
|
||||
- **Упоминание "поставщик"** → WHOLESALE_DOMAIN.md
|
||||
- **Упоминание "логистика"** → LOGIST_DOMAIN.md
|
||||
- **Упоминание "GraphQL"** → GRAPHQL_SCHEMA_RULES.md
|
||||
- **Упоминание "компонент"** → COMPONENT_ARCHITECTURE.md
|
||||
- **Упоминание "поставки"** → SUPPLY_CHAIN_WORKFLOW.md
|
||||
|
||||
---
|
||||
|
||||
## 8. ИНСТРУМЕНТЫ И КОМАНДЫ
|
||||
|
||||
### ОТЧЕТНОСТЬ
|
||||
|
||||
После выполнения всегда показывать:
|
||||
```
|
||||
✅ СДЕЛАНО:
|
||||
- Создал файл X
|
||||
- Добавил функцию Y
|
||||
|
||||
❌ НЕ ТРОГАЛ:
|
||||
- Файл Z
|
||||
- Логику W
|
||||
|
||||
⚠️ ПРОБЛЕМЫ:
|
||||
- ESLint warning в строке N
|
||||
```
|
||||
|
||||
### КОМАНДЫ ОТКАТА ЧЕРЕЗ КОММЕНТАРИИ
|
||||
|
||||
#### Основная команда:
|
||||
```
|
||||
"откати [описание] через комментарии"
|
||||
```
|
||||
|
||||
**Примеры:**
|
||||
|
||||
**Примеры использования:**
|
||||
- `"откати центрирование поиска через комментарии"`
|
||||
- `"откати изменения кнопки через комментарии"`
|
||||
- `"откати новую логику через комментарии"`
|
||||
|
||||
**Дополнительные команды:**
|
||||
#### Алгоритм выполнения:
|
||||
|
||||
**ЭТАП 1: ВОССТАНОВЛЕНИЕ ИСХОДНОГО КОДА**
|
||||
1. Найти измененный код в текущих файлах
|
||||
2. Извлечь исходный код из git истории
|
||||
3. Восстановить исходную функциональность
|
||||
|
||||
**ЭТАП 2: СОЗДАНИЕ СИСТЕМЫ ПЕРЕКЛЮЧЕНИЯ**
|
||||
4. Оставить **Вариант 1** (исходный) - активным
|
||||
5. Добавить **Вариант 2** (измененный) в комментариях
|
||||
6. Добавить четкие описания для каждого варианта
|
||||
|
||||
**Пример структуры кода:**
|
||||
```jsx
|
||||
// Вариант 1: Исходный (активный)
|
||||
<div className="flex items-center justify-between">
|
||||
{/* исходный код */}
|
||||
</div>
|
||||
|
||||
// Вариант 2: Измененный (для быстрого переключения)
|
||||
/*
|
||||
<div className="flex justify-center">
|
||||
{/* измененный код */}
|
||||
</div>
|
||||
*/
|
||||
```
|
||||
|
||||
#### Дополнительные команды:
|
||||
- `"очисти комментарии"` - удалить закомментированные варианты
|
||||
- `"переключи на вариант 2"` - активировать закомментированный код
|
||||
- `"покажи варианты"` - показать доступные варианты
|
||||
|
||||
> 📖 **Подробнее**: см. раздел 6.4 в `legacy-rules/interaction-integrity-rules.md`
|
||||
#### ПРАВИЛА ПРИМЕНЕНИЯ:
|
||||
- ✅ **Использовать для UI экспериментов** и небольших логических изменений
|
||||
- ✅ **Всегда добавлять четкие комментарии** с описанием вариантов
|
||||
- ✅ **Очищать комментарии перед финальным коммитом**
|
||||
- ❌ **Не использовать для изменений архитектуры** или критической логики
|
||||
|
||||
## 💾 РАБОТА С КОНТЕКСТОМ
|
||||
### ДОКУМЕНТИРОВАНИЕ ИЗМЕНЕНИЙ
|
||||
|
||||
### Файлы для сохранения контекста:
|
||||
#### При любых изменениях документировать:
|
||||
- **ЧТО** изменено (конкретные файлы и функции)
|
||||
- **ПОЧЕМУ** изменено (обоснование решения)
|
||||
- **КТО** принял решение об изменении (пользователь/автоматически)
|
||||
- **КОГДА** изменено (временная метка)
|
||||
|
||||
- **`current-session.md`** - текущая сессия работы (активные задачи, решения, контекст)
|
||||
- **`CLAUDE.md`** - системные правила и команды (этот файл)
|
||||
- **TodoWrite инструмент** - для отслеживания текущих задач в рамках сессии
|
||||
**Формат документирования:**
|
||||
```
|
||||
📝 ИЗМЕНЕНИЕ ЗАФИКСИРОВАНО:
|
||||
- ДАТА: [когда]
|
||||
- ЧТО: [что именно изменено]
|
||||
- ПРИЧИНА: [обоснование]
|
||||
- РЕШЕНИЕ: [кто одобрил]
|
||||
```
|
||||
|
||||
### При потере контекста:
|
||||
### АНАЛИЗ ПРИМЕРОВ КОДА
|
||||
|
||||
1. **Первым делом прочитать**: `current-session.md`
|
||||
2. **Проверить статус задач**: через TodoWrite
|
||||
3. **Восстановить контекст**: из истории изменений в current-session.md
|
||||
#### Трехуровневый анализ примеров:
|
||||
1. **📋 СОДЕРЖАТЕЛЬНЫЙ** - что делает код (функциональность, логика, данные)
|
||||
2. **🏗️ АРХИТЕКТУРНЫЙ** - как организован (структура, взаимосвязи, позиционирование)
|
||||
3. **🎨 СТИЛЕВОЙ** - как выглядит (CSS классы, анимации, цвета)
|
||||
|
||||
### Рекомендации для длинных сессий:
|
||||
#### Алгоритм анализа примера:
|
||||
1. **Прочитать** весь код компонента-примера
|
||||
2. **Понять архитектуру** - где элемент размещен относительно других
|
||||
3. **Понять логику** - почему именно так структурировано
|
||||
4. **Адаптировать** к текущей задаче - применить принципы, не копировать
|
||||
5. **Проверить** соответствие правилам проекта
|
||||
|
||||
- Обновлять `current-session.md` после каждой важной задачи
|
||||
- Фиксировать принятые решения и обоснования
|
||||
- Документировать обнаруженные проблемы и их решения
|
||||
- Использовать `--resume` флаг для продолжения сессий
|
||||
#### Стоп-вопросы перед реализацией:
|
||||
- "Понимаю ли я **архитектуру** этого решения?"
|
||||
- "Где именно должен располагаться элемент в **общей структуре**?"
|
||||
- "Какова **семантическая роль** этого элемента?"
|
||||
- "Как это решение **адаптируется** к моей текущей задаче?"
|
||||
|
||||
## 🚨 НАПОМИНАНИЕ
|
||||
### ИЗВЛЕЧЕННЫЕ УРОКИ И АНТИ-ПАТТЕРНЫ
|
||||
|
||||
**Этот файл служит для корректной работы system-reminder'ов. Все детальные правила находятся в `legacy-rules/rules-complete1.md` и `legacy-rules/rules-complete2.md`! Новая архитектура правил в папке `docs/` находится в разработке.**
|
||||
#### КРИТИЧЕСКИЕ ОШИБКИ В АНАЛИЗЕ UI КОМПОНЕНТОВ:
|
||||
|
||||
**CASE STUDY: Ошибка с плавающей кнопкой из UI Kit**
|
||||
|
||||
**❌ ОШИБКА**: При добавлении кнопки "🌟 Вариант 1: Плавающая кнопка слева":
|
||||
1. **Поверхностный анализ**: Скопировал только стили кнопки
|
||||
2. **Игнорирование архитектуры**: Не заметил, что кнопка в **отдельном контейнере**
|
||||
3. **Неправильное размещение**: Добавил как часть блока контента
|
||||
4. **Непонимание термина**: "Плавающая" = независимая от контента, между элементами
|
||||
|
||||
#### ОБЯЗАТЕЛЬНЫЙ ЧЕК-ЛИСТ ДЛЯ UI KIT КОМПОНЕНТОВ:
|
||||
```
|
||||
🔍 ПЕРЕД РЕАЛИЗАЦИЕЙ:
|
||||
□ Прочитал ВЕСЬ код компонента-примера
|
||||
□ Понял архитектуру размещения в layout
|
||||
□ Определил семантическую роль элемента
|
||||
□ Понял взаимосвязи с соседними элементами
|
||||
□ Адаптировал принципы к текущей задаче
|
||||
□ Проверил соответствие правилам проекта
|
||||
```
|
||||
|
||||
#### АНТИ-ПАТТЕРНЫ:
|
||||
- **"Быстрое копирование"** - копировать стили без понимания архитектуры
|
||||
- **"Частичный анализ"** - читать только нужную часть кода
|
||||
- **"Буквальное применение"** - использовать без адаптации к контексту
|
||||
- **"Игнорирование контейнеров"** - не обращать внимание на DOM-структуру
|
||||
|
||||
#### ПРАВИЛЬНЫЕ ПАТТЕРНЫ:
|
||||
- **"Архитектурный анализ первым"** - понять структуру, потом стили
|
||||
- **"Контекстная адаптация"** - применять принципы, а не код буквально
|
||||
- **"Семантическое понимание"** - осознавать роль каждого элемента
|
||||
- **"Итеративная проверка"** - сверяться с примером на каждом шаге
|
||||
|
||||
### БЫСТРЫЕ ПРЕФИКСЫ
|
||||
|
||||
- [STRICT] - строгий режим робота
|
||||
- [CHECK] - только проверить
|
||||
- [EXPLAIN] - только объяснить
|
||||
- [FIX-ONLY] - только исправить конкретную ошибку
|
||||
|
||||
### АКТИВНЫЕ ДОКУМЕНТЫ
|
||||
|
||||
- README.md - техническая документация
|
||||
- package.json - зависимости и скрипты
|
||||
- prisma/schema.prisma - модели данных
|
||||
- src/graphql/typedefs.ts - GraphQL схема
|
||||
- .env - переменные окружения (не коммитить!)
|
||||
- docker-compose.yml - настройки Docker
|
||||
- src/lib/prisma.ts - клиент базы данных
|
||||
|
||||
### РАБОТА С КОНТЕКСТОМ
|
||||
|
||||
#### Файлы контекста:
|
||||
- **current-session.md** - текущие задачи и решения
|
||||
- **CLAUDE.md** - эти правила (загружаются автоматически)
|
||||
- **TodoWrite** - инструмент для отслеживания задач
|
||||
|
||||
#### При потере контекста:
|
||||
1. Прочитать current-session.md
|
||||
2. Проверить TodoWrite
|
||||
3. Спросить у пользователя о текущей задаче
|
||||
|
||||
---
|
||||
|
||||
# Важные напоминания для Claude Code
|
||||
Делай только то, что просят; ни больше, ни меньше.
|
||||
НИКОГДА не создавай файлы, если они не абсолютно необходимы для достижения цели.
|
||||
ВСЕГДА отдавай предпочтение редактированию существующего файла, а не созданию нового.
|
||||
НИКОГДА не создавай проактивно файлы документации (*.md) или README файлы. Создавай файлы документации только по явной просьбе пользователя.
|
368
README.md
368
README.md
@ -1,18 +1,186 @@
|
||||
# Sfera V - Управление бизнесом
|
||||
# 🌐 Sfera - B2B Marketplace Platform
|
||||
|
||||
Платформа для управления различными типами бизнеса: фулфилмент, селлеры, логистика, оптовики.
|
||||
**Комплексная платформа для управления бизнес-процессами в сфере электронной коммерции**
|
||||
|
||||
## Новые возможности
|
||||
[](https://nextjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://www.prisma.io/)
|
||||
[](https://graphql.org/)
|
||||
|
||||
### 🏪 Склад Wildberries для селлеров
|
||||
## 🏗️ Архитектура системы
|
||||
|
||||
Новый раздел для селлеров, позволяющий:
|
||||
Sfera - это многомодульная B2B платформа, объединяющая четыре типа участников:
|
||||
|
||||
- **Просмотр остатков** товаров на всех складах WB в реальном времени
|
||||
- **Статистика по складам** - общее количество товаров, остатки, товары в пути
|
||||
- **Фильтрация и поиск** товаров по названию, артикулу, бренду
|
||||
- **Детальная информация** по каждому складу отдельно
|
||||
- **Красивые карточки товаров** с изображениями и статусами остатков
|
||||
- **👔 Seller** - Продавцы на маркетплейсах
|
||||
- **📦 Fulfillment** - Фулфилмент-центры
|
||||
- **🚛 Logist** - Логистические компании
|
||||
- **🏪 Wholesale** - Поставщики товаров
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Предварительные требования
|
||||
- Node.js 20+
|
||||
- PostgreSQL 14+
|
||||
- npm или yarn
|
||||
|
||||
### Установка
|
||||
|
||||
```bash
|
||||
# Клонирование проекта
|
||||
git clone <repository-url>
|
||||
cd sfera
|
||||
|
||||
# Установка зависимостей
|
||||
npm install
|
||||
|
||||
# Настройка базы данных
|
||||
cp .env.example .env
|
||||
# Отредактируйте DATABASE_URL в .env
|
||||
|
||||
# Инициализация БД
|
||||
npx prisma db push
|
||||
npm run db:seed
|
||||
|
||||
# Запуск в режиме разработки
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Приложение будет доступно по адресу: http://localhost:3000
|
||||
|
||||
### 👤 Администратор по умолчанию
|
||||
|
||||
При первом запуске автоматически создается администратор:
|
||||
- **Логин**: `admin`
|
||||
- **Пароль**: `admin123`
|
||||
- **Email**: `admin@sferav.com`
|
||||
|
||||
⚠️ **ОБЯЗАТЕЛЬНО смените пароль после первого входа для безопасности!**
|
||||
|
||||
### 🔄 Автоматическая инициализация БД
|
||||
|
||||
**Система умно инициализируется:**
|
||||
- ✅ Проверяет существующие данные
|
||||
- ✅ Не создает дубликаты при повторном запуске
|
||||
- ✅ Автоматическая инициализация при первом запуске приложения
|
||||
- ✅ Создание 20 базовых категорий товаров
|
||||
- ✅ Настройка администратора системы
|
||||
|
||||
## 🔧 Команды разработчика
|
||||
|
||||
### 🏃♂️ Основные команды
|
||||
```bash
|
||||
# Разработка с turbopack
|
||||
npm run dev
|
||||
|
||||
# Сборка для production
|
||||
npm run build
|
||||
|
||||
# Запуск production
|
||||
npm start
|
||||
```
|
||||
|
||||
### 🗄️ База данных
|
||||
```bash
|
||||
# Инициализация БД (создание админа и категорий)
|
||||
npm run db:seed
|
||||
|
||||
# Полный сброс БД и пересоздание данных
|
||||
npm run db:reset
|
||||
|
||||
# Применение миграций и генерация клиента
|
||||
npx prisma db push
|
||||
|
||||
# Prisma Studio
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
### 🧹 Качество кода
|
||||
```bash
|
||||
# Проверка ESLint
|
||||
npm run lint
|
||||
|
||||
# Исправление ESLint ошибок
|
||||
npm run lint:fix
|
||||
|
||||
# Форматирование Prettier
|
||||
npm run format
|
||||
|
||||
# Проверка форматирования
|
||||
npm run format:check
|
||||
```
|
||||
|
||||
## 🏛️ Архитектура проекта
|
||||
|
||||
### 📁 Структура каталогов
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # API endpoints
|
||||
│ ├── dashboard/ # Дашборды по типам организаций
|
||||
│ ├── supplies/ # Управление поставками
|
||||
│ ├── wb-warehouse/ # Склад WB (только для SELLER)
|
||||
│ ├── employees/ # Система управления персоналом
|
||||
│ ├── market/ # B2B маркетплейс
|
||||
│ └── ...
|
||||
├── components/ # React компоненты
|
||||
│ ├── dashboard/ # Компоненты дашбордов
|
||||
│ ├── supplies/ # Компоненты поставок
|
||||
│ ├── wb-warehouse/ # Компоненты интерфейса склада WB
|
||||
│ ├── auth/ # Система авторизации
|
||||
│ └── ui/ # UI Kit на Radix UI
|
||||
├── graphql/ # GraphQL слой
|
||||
│ ├── typedefs.ts # GraphQL схема
|
||||
│ ├── resolvers.ts # Резолверы
|
||||
│ ├── security/ # Система безопасности
|
||||
│ └── ...
|
||||
├── lib/ # Утилиты и конфигурации
|
||||
│ └── seed-init.ts # Автоматическая инициализация
|
||||
├── hooks/ # React хуки
|
||||
├── services/ # Внешние сервисы
|
||||
│ └── wildberries-service.ts # Интеграция с API WB
|
||||
└── types/ # TypeScript типы
|
||||
```
|
||||
|
||||
### 🔧 Технологический стек
|
||||
|
||||
#### **Frontend**
|
||||
- **Next.js 15.4.1** - React фреймворк с App Router
|
||||
- **React 19.1.0** - UI библиотека
|
||||
- **TypeScript 5** - Типизация
|
||||
- **Tailwind CSS 4** - Стилизация
|
||||
- **Radix UI** - Компоненты интерфейса
|
||||
|
||||
#### **Backend & API**
|
||||
- **GraphQL** - API слой с Apollo Server
|
||||
- **Prisma 6.12.0** - ORM для работы с БД
|
||||
- **PostgreSQL** - Основная база данных
|
||||
- **JWT** - Авторизация и безопасность
|
||||
|
||||
#### **Интеграции**
|
||||
- **Wildberries API** - Интеграция с маркетплейсом
|
||||
- **Ozon API** - Интеграция с маркетплейсом
|
||||
- **DaData API** - Валидация ИНН организаций и работа с реквизитами
|
||||
- **SMS Aero** - Отправка SMS-сообщений для авторизации
|
||||
- **AWS S3** - Хранение файлов
|
||||
|
||||
#### **Инфраструктура**
|
||||
- **Docker** - Контейнеризация
|
||||
- **Husky** - Git hooks
|
||||
- **ESLint + Prettier** - Линтинг и форматирование
|
||||
|
||||
## 🏪 Новые возможности
|
||||
|
||||
### 🛒 **Склад Wildberries для селлеров**
|
||||
|
||||
**Доступ:** Только для пользователей с типом организации **SELLER**
|
||||
|
||||
Новый раздел позволяет:
|
||||
|
||||
- **📊 Просмотр остатков** товаров на всех складах WB в реальном времени
|
||||
- **📈 Статистика по складам** - общее количество товаров, остатки, товары в пути
|
||||
- **🔍 Фильтрация и поиск** товаров по названию, артикулу, бренду
|
||||
- **📋 Детальная информация** по каждому складу отдельно
|
||||
- **🎨 Красивые карточки товаров** с изображениями и статусами остатков
|
||||
|
||||
#### Как использовать:
|
||||
1. Настройте API ключ Wildberries в разделе "Настройки" → "API"
|
||||
@ -20,73 +188,149 @@
|
||||
3. Система автоматически загрузит актуальные остатки с вашего аккаунта WB
|
||||
|
||||
#### Технические особенности:
|
||||
- Интеграция с официальным API Wildberries
|
||||
- Поддержка всех типов складов WB
|
||||
- Кэширование данных для быстрой работы
|
||||
- Адаптивный дизайн в стиле платформы
|
||||
- ✅ Интеграция с официальным API Wildberries
|
||||
- ✅ Поддержка всех типов складов WB
|
||||
- ✅ Кэширование данных для быстрой работы
|
||||
- ✅ Адаптивный дизайн в стиле платформы
|
||||
|
||||
## База данных и инициализация
|
||||
## 🔐 Система безопасности
|
||||
|
||||
### 🛠 Команды для работы с БД
|
||||
Проект включает комплексную систему безопасности:
|
||||
|
||||
- `npm run db:seed` - Инициализация БД (создание админа и категорий)
|
||||
- `npm run db:reset` - Полный сброс БД и пересоздание данных
|
||||
- `npm run postinstall` - Генерация Prisma Client
|
||||
- **🛡️ Аудит действий** - Логирование всех операций
|
||||
- **🔍 Обнаружение угроз** - Автоматическое выявление подозрительной активности
|
||||
- **🚨 Мониторинг доступа** - Контроль доступа к данным партнеров
|
||||
- **📊 Security Dashboard** - Панель управления безопасностью
|
||||
|
||||
### 👤 Администратор по умолчанию
|
||||
## 📊 Основные модули
|
||||
|
||||
При первом запуске автоматически создается админ:
|
||||
- **Логин**: `admin`
|
||||
- **Пароль**: `admin123`
|
||||
- **Email**: `admin@sferav.com`
|
||||
### 🏪 **Управление поставками**
|
||||
- Многоуровневая система заказов
|
||||
- Интеграция поставщик → логистика → фулфилмент
|
||||
- Отслеживание статусов поставок
|
||||
- Управление рецептурами товаров
|
||||
|
||||
⚠️ **Обязательно смените пароль после первого входа!**
|
||||
### 👥 **Система партнерства**
|
||||
- Реферальная программа
|
||||
- Автоматическое создание бизнес-партнерств
|
||||
- Управление контрагентами
|
||||
- B2B мессенджер
|
||||
|
||||
### 📂 Категории товаров
|
||||
### 📈 **Аналитика и статистика**
|
||||
- Статистика продаж Wildberries/Ozon
|
||||
- Анализ рекламных кампаний
|
||||
- Складская аналитика
|
||||
- Отчеты по сотрудникам
|
||||
|
||||
Автоматически создается 20 базовых категорий:
|
||||
- Одежда и обувь, Косметика и парфюмерия, Дом и сад
|
||||
- Детские товары, Спорт и отдых, Электроника
|
||||
- И другие популярные категории...
|
||||
### 🏭 **Управление складом**
|
||||
- Система остатков и резервов
|
||||
- Управление возвратами ПВЗ
|
||||
- Складская логистика
|
||||
- Инвентаризация
|
||||
|
||||
### 🔄 Автоматическая инициализация
|
||||
## 🔌 API и интеграции
|
||||
|
||||
База данных инициализируется автоматически при:
|
||||
- Первом запуске приложения
|
||||
- Запуске команды `npm run db:seed`
|
||||
- Сбросе БД через `npm run db:reset`
|
||||
### GraphQL API
|
||||
Основной API построен на GraphQL со следующими возможностями:
|
||||
- **Аутентификация** через SMS-коды
|
||||
- **CRUD операции** для всех сущностей
|
||||
- **Система прав доступа** по типу организации
|
||||
- **Real-time подписки** для чатов
|
||||
|
||||
Система умно проверяет существующие данные и не создает дубликаты.
|
||||
### Внешние API
|
||||
- **Wildberries API** - статистика, товары, кампании, остатки склада
|
||||
- **Ozon API** - интеграция с маркетплейсом
|
||||
- **DaData API** - валидация ИНН организаций, получение реквизитов компаний
|
||||
- **SMS Aero** - отправка SMS-кодов для авторизации пользователей
|
||||
|
||||
## Структура проекта
|
||||
|
||||
- `src/app/wb-warehouse/` - Страница склада WB
|
||||
- `prisma/seed.js` - Скрипт инициализации БД
|
||||
- `src/lib/seed-init.ts` - Автоматическая инициализация
|
||||
- `src/components/wb-warehouse/` - Компоненты интерфейса склада
|
||||
- `src/services/wildberries-service.ts` - Интеграция с API WB
|
||||
|
||||
## Технологии
|
||||
|
||||
- Next.js 15
|
||||
- React 18
|
||||
- TypeScript
|
||||
- GraphQL
|
||||
- Prisma
|
||||
- TailwindCSS
|
||||
- Shadcn/ui
|
||||
|
||||
## Установка и запуск
|
||||
## 🧪 Тестирование и отладка
|
||||
|
||||
### Скрипты отладки
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
# Анализ данных фулфилмента
|
||||
node scripts/analyze-fulfillment-supplies.cjs
|
||||
|
||||
# Проверка типов поставок
|
||||
node scripts/check-supply-order-types.cjs
|
||||
|
||||
# Очистка тестовых данных
|
||||
node scripts/clear-fulfillment-data.cjs
|
||||
```
|
||||
|
||||
## API интеграции
|
||||
### Health Check
|
||||
```bash
|
||||
# Проверка состояния API
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
- Wildberries API для получения остатков и информации о складах
|
||||
- DaData для работы с организациями
|
||||
- SMS Aero для отправки SMS
|
||||
## 📚 Документация
|
||||
|
||||
Доступ к разделу "Склад ВБ" имеют только пользователи с типом организации "SELLER".
|
||||
Детальная документация проекта находится в:
|
||||
- **`docs/`** - Новая модульная архитектура документации
|
||||
- **`legacy-rules/`** - Бизнес-правила и процессы
|
||||
- **`CLAUDE.md`** - Системные правила разработки
|
||||
- **`prisma/seed.js`** - Скрипт инициализации БД
|
||||
|
||||
## 🐳 Docker
|
||||
|
||||
```bash
|
||||
# Сборка образа
|
||||
docker build -t sfera .
|
||||
|
||||
# Запуск с docker-compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 🤝 Участие в разработке
|
||||
|
||||
### Стиль кода
|
||||
- **ESLint** - обязательное соблюдение правил
|
||||
- **Prettier** - автоматическое форматирование
|
||||
- **TypeScript** - строгая типизация
|
||||
- **Conventional Commits** - стандарт коммитов
|
||||
|
||||
### Git Flow
|
||||
1. Создание feature-ветки
|
||||
2. Разработка с соблюдением линтинга
|
||||
3. Тестирование изменений
|
||||
4. Pull Request с ревью
|
||||
|
||||
## 🔧 Переменные окружения
|
||||
|
||||
```bash
|
||||
# База данных
|
||||
DATABASE_URL="postgresql://user:pass@localhost:5432/sfera"
|
||||
|
||||
# JWT
|
||||
JWT_SECRET="your-secret-key"
|
||||
|
||||
# API ключи маркетплейсов
|
||||
WB_API_TOKEN="wildberries-token"
|
||||
OZON_CLIENT_ID="ozon-client-id"
|
||||
OZON_API_KEY="ozon-api-key"
|
||||
|
||||
# DaData API
|
||||
DADATA_API_KEY="dadata-api-key"
|
||||
DADATA_SECRET="dadata-secret"
|
||||
|
||||
# SMS Aero
|
||||
SMS_AERO_EMAIL="your-email"
|
||||
SMS_AERO_API_KEY="sms-aero-api-key"
|
||||
|
||||
# S3 для файлов
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
AWS_SECRET_ACCESS_KEY="secret-key"
|
||||
```
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
При возникновении проблем:
|
||||
1. Проверьте статус систем через `/api/health`
|
||||
2. Просмотрите логи в `dev.log`
|
||||
3. Обратитесь к документации в `docs/`
|
||||
|
||||
---
|
||||
|
||||
**Лицензия:** Частная разработка
|
||||
**Статус:** Активная разработка
|
||||
**Версия:** 0.2.0
|
348
docs/business-processes/SUPPLY_CHAIN_WORKFLOW_V2.md
Normal file
348
docs/business-processes/SUPPLY_CHAIN_WORKFLOW_V2.md
Normal file
@ -0,0 +1,348 @@
|
||||
# 🚚 WORKFLOW ЦЕПОЧКИ ПОСТАВОК SFERA v2.0
|
||||
|
||||
> **⚠️ ВАЖНО:** Этот документ описывает НОВУЮ архитектуру системы поставок с разделением на отдельные типы. Для старой системы см. SUPPLY_CHAIN_WORKFLOW.md
|
||||
|
||||
## 🎯 ОБЗОР НОВОЙ СИСТЕМЫ
|
||||
|
||||
Система поставок SFERA v2.0 включает **5 типов поставок**, разделенных на две категории:
|
||||
|
||||
### 📦 **ПОСТАВКИ НА ФУЛФИЛМЕНТ**
|
||||
- `GoodsSupplyOrder` - товары селлера → склад ФФ
|
||||
- `FulfillmentConsumableSupplyOrder` - расходники ФФ → склад ФФ
|
||||
- `SellerConsumableSupplyOrder` - расходники селлера → склад ФФ
|
||||
|
||||
### 🛒 **ПОСТАВКИ НА МАРКЕТПЛЕЙСЫ**
|
||||
- `OzonSupplyOrder` - готовые продукты → Ozon
|
||||
- `WildberriesSupplyOrder` - готовые продукты → Wildberries
|
||||
|
||||
---
|
||||
|
||||
## 🔄 WORKFLOW ПО ТИПАМ ПОСТАВОК
|
||||
|
||||
### 1️⃣ **WORKFLOW: Поставки расходников ФФ**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[ФФ создает заказ расходников] --> B[PENDING]
|
||||
B --> C{Поставщик одобряет?}
|
||||
C -->|Да| D[SUPPLIER_APPROVED]
|
||||
C -->|Нет| X[CANCELLED]
|
||||
D --> E{Логистика назначена?}
|
||||
E -->|Да| F[LOGISTICS_CONFIRMED]
|
||||
E -->|Нет| D
|
||||
F --> G[Поставщик отгружает]
|
||||
G --> H[SHIPPED]
|
||||
H --> I[IN_TRANSIT]
|
||||
I --> J[ФФ принимает на склад]
|
||||
J --> K[DELIVERED]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style K fill:#c8e6c9
|
||||
style X fill:#ffcdd2
|
||||
```
|
||||
|
||||
**Участники:**
|
||||
- 🏭 **Фулфилмент** - создатель, получатель
|
||||
- 🏪 **Поставщик (WHOLESALE)** - одобрение, отгрузка
|
||||
- 🚛 **Логистика (LOGIST)** - доставка
|
||||
|
||||
**Особенности:**
|
||||
- ✅ ФФ видит все детали + устанавливает цены продажи селлерам
|
||||
- ✅ Поставщик видит товары/количества, НЕ видит цены продажи ФФ
|
||||
- ✅ Показывается сразу после создания
|
||||
|
||||
### 2️⃣ **WORKFLOW: Поставки товаров селлера**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Селлер создает заказ товаров] --> B[PENDING]
|
||||
B --> C{Поставщик одобряет?}
|
||||
C -->|Да| D[SUPPLIER_APPROVED]
|
||||
C -->|Нет| X[CANCELLED]
|
||||
D --> E{Логистика назначена?}
|
||||
E -->|Да| F[LOGISTICS_CONFIRMED]
|
||||
E -->|Нет| D
|
||||
F --> G[Поставщик отгружает]
|
||||
G --> H[SHIPPED]
|
||||
H --> I[IN_TRANSIT]
|
||||
I --> J[ФФ принимает + обрабатывает]
|
||||
J --> K[DELIVERED]
|
||||
|
||||
style A fill:#fff3e0
|
||||
style K fill:#c8e6c9
|
||||
style X fill:#ffcdd2
|
||||
```
|
||||
|
||||
**Участники:**
|
||||
- 🛒 **Селлер** - создатель, владелец товара
|
||||
- 🏪 **Поставщик (WHOLESALE)** - поставка
|
||||
- 🚛 **Логистика (LOGIST)** - доставка
|
||||
- 🏭 **Фулфилмент** - получатель, обработка
|
||||
|
||||
**Особенности:**
|
||||
- ✅ Селлер видит свои товары + рецептуры + закупочные цены
|
||||
- ✅ ФФ видит товары + рецептуры + услуги, НЕ видит закупочные цены селлера
|
||||
- ✅ Поставщик видит товары + количества, НЕ видит рецептуры
|
||||
- ✅ Расходники селлера идут **в состав продукта**, не отслеживаются отдельно
|
||||
|
||||
### 3️⃣ **WORKFLOW: Поставки расходников селлера**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Селлер заказывает свои расходники] --> B[PENDING]
|
||||
B --> C{Поставщик одобряет?}
|
||||
C -->|Да| D[SUPPLIER_APPROVED]
|
||||
C -->|Нет| X[CANCELLED]
|
||||
D --> E{Логистика назначена?}
|
||||
E -->|Да| F[LOGISTICS_CONFIRMED]
|
||||
E -->|Нет| D
|
||||
F --> G[Поставщик отгружает]
|
||||
G --> H[SHIPPED]
|
||||
H --> I[IN_TRANSIT]
|
||||
I --> J[ФФ принимает НА ХРАНЕНИЕ]
|
||||
J --> K[DELIVERED]
|
||||
|
||||
style A fill:#f3e5f5
|
||||
style K fill:#c8e6c9
|
||||
style X fill:#ffcdd2
|
||||
```
|
||||
|
||||
**Участники:**
|
||||
- 🛒 **Селлер** - создатель, владелец расходников
|
||||
- 🏪 **Поставщик (WHOLESALE)** - поставка
|
||||
- 🚛 **Логистика (LOGIST)** - доставка
|
||||
- 🏭 **Фулфилмент** - хранитель (НЕ владелец)
|
||||
|
||||
**Особенности:**
|
||||
- ✅ Селлер видит свои расходники + закупочные цены
|
||||
- ✅ ФФ видит факт хранения + количества, НЕ видит закупочные цены селлера
|
||||
- ✅ Срок хранения + права доступа настраиваются
|
||||
- ✅ Используются селлером в рецептурах своих товаров
|
||||
|
||||
---
|
||||
|
||||
## 📊 СТАТУСЫ И ПЕРЕХОДЫ
|
||||
|
||||
### **SupplyOrderStatus (поставки НА фулфилмент)**
|
||||
|
||||
| Статус | Описание | Ответственный | Действия |
|
||||
|--------|----------|---------------|----------|
|
||||
| `PENDING` | Ожидает одобрения поставщика | Поставщик | Одобрить/Отклонить |
|
||||
| `SUPPLIER_APPROVED` | Одобрено поставщиком | Логистика | Назначить маршрут |
|
||||
| `LOGISTICS_CONFIRMED` | Логистика подтверждена | Поставщик | Отгрузить товар |
|
||||
| `SHIPPED` | Отгружено | Система | Автоматический переход |
|
||||
| `IN_TRANSIT` | В пути | Логистика | Отслеживание доставки |
|
||||
| `DELIVERED` | Доставлено | ФФ | Принять на склад |
|
||||
| `CANCELLED` | Отменено | Любой участник | Указать причину |
|
||||
|
||||
### **MarketplaceSupplyStatus (поставки НА маркетплейсы)**
|
||||
|
||||
| Статус | Описание | Ответственный | Действия |
|
||||
|--------|----------|---------------|----------|
|
||||
| `PLANNED` | Запланирована | ФФ | Подготовить товары |
|
||||
| `PREPARED` | Подготовлена | ФФ | Отгрузить |
|
||||
| `SHIPPED_TO_MARKETPLACE` | Отгружена | Маркетплейс | Принять товар |
|
||||
| `ACCEPTED_BY_MARKETPLACE` | Принята | Система | Обновить остатки |
|
||||
| `CANCELLED` | Отменена | ФФ/Маркетплейс | Указать причину |
|
||||
|
||||
---
|
||||
|
||||
## 🎭 РОЛИ И ПРАВА ДОСТУПА
|
||||
|
||||
### 🏭 **ФУЛФИЛМЕНТ**
|
||||
|
||||
**Может создавать:**
|
||||
- ✅ Поставки расходников ФФ
|
||||
- ✅ Поставки на маркетплейсы
|
||||
|
||||
**Может видеть:**
|
||||
- ✅ Свои поставки расходников: все детали + цены продажи
|
||||
- ✅ Товарные поставки селлеров: товары + рецептуры, НЕ закупочные цены
|
||||
- ✅ Расходники селлеров на хранении: количества, НЕ закупочные цены
|
||||
|
||||
### 🛒 **СЕЛЛЕР**
|
||||
|
||||
**Может создавать:**
|
||||
- ✅ Товарные поставки
|
||||
- ✅ Поставки расходников селлера
|
||||
|
||||
**Может видеть:**
|
||||
- ✅ Свои товарные поставки: все детали + рецептуры + закупочные цены
|
||||
- ✅ Свои расходники: все детали + закупочные цены
|
||||
- ❌ Чужие поставки
|
||||
- ❌ Поставки расходников ФФ
|
||||
|
||||
### 🏪 **ПОСТАВЩИК (WHOLESALE)**
|
||||
|
||||
**Может видеть:**
|
||||
- ✅ Заказы к себе: товары + количества
|
||||
- ❌ Рецептуры товаров
|
||||
- ❌ Цены продажи ФФ селлерам
|
||||
- ❌ Услуги ФФ
|
||||
|
||||
### 🚛 **ЛОГИСТИКА (LOGIST)**
|
||||
|
||||
**Может видеть:**
|
||||
- ✅ Маршруты + объемы + вес
|
||||
- ❌ Коммерческие данные (цены, услуги)
|
||||
- ❌ Рецептуры товаров
|
||||
|
||||
---
|
||||
|
||||
## 🌐 ИНТЕРФЕЙСЫ СИСТЕМЫ
|
||||
|
||||
### 📦 **Кабинет фулфилмента**
|
||||
|
||||
**URL:** `/fulfillment-supplies/`
|
||||
|
||||
**Вкладки:**
|
||||
- `ff-consumables` - поставки расходников ФФ (создание + просмотр)
|
||||
- `seller-consumables` - расходники селлеров на хранении
|
||||
- `goods?status=new` - новые товарные поставки
|
||||
- `goods?status=receiving` - товары в приемке
|
||||
- `goods?status=accepted` - принятые товары
|
||||
|
||||
### 🛒 **Кабинет селлера**
|
||||
|
||||
**URL:** `/seller-supplies/`
|
||||
|
||||
**Вкладки:**
|
||||
- `my-goods` - мои товарные поставки
|
||||
- `my-consumables` - мои расходники
|
||||
- `create-goods` - создать поставку товаров
|
||||
- `create-consumables` - заказать расходники
|
||||
|
||||
### 🛍️ **Кабинет маркетплейсов**
|
||||
|
||||
**URL:** `/marketplace-supplies/`
|
||||
|
||||
**Вкладки:**
|
||||
- `ozon` - поставки на Ozon
|
||||
- `wildberries` - поставки на Wildberries
|
||||
- `create-ozon` - создать поставку на Ozon
|
||||
- `create-wildberries` - создать поставку на WB
|
||||
|
||||
---
|
||||
|
||||
## ⚡ ОСОБЕННОСТИ РЕАЛИЗАЦИИ
|
||||
|
||||
### 🔄 **Дополнение данных по этапам**
|
||||
|
||||
Каждая поставка - это **одна запись**, которая дополняется участниками:
|
||||
|
||||
```typescript
|
||||
// Создание (селлер/ФФ)
|
||||
supply = {
|
||||
id: "...",
|
||||
status: "PENDING",
|
||||
sellerId: "...", // кто создал
|
||||
requestedDate: "...", // когда нужно
|
||||
items: [...] // что заказано
|
||||
}
|
||||
|
||||
// Одобрение (поставщик)
|
||||
supply = {
|
||||
...supply,
|
||||
status: "SUPPLIER_APPROVED",
|
||||
supplierId: "...", // кто одобрил
|
||||
approvedAt: "...", // когда одобрил
|
||||
packagesCount: 5, // уточненные параметры
|
||||
estimatedVolume: 2.5
|
||||
}
|
||||
|
||||
// Назначение логистики (ФФ)
|
||||
supply = {
|
||||
...supply,
|
||||
status: "LOGISTICS_CONFIRMED",
|
||||
logisticsPartnerId: "...", // кто повезет
|
||||
routeId: "...", // маршрут
|
||||
logisticsCost: 1500 // стоимость
|
||||
}
|
||||
```
|
||||
|
||||
### 🔐 **Фильтрация по безопасности**
|
||||
|
||||
Каждый resolver применяет фильтрацию по роли:
|
||||
|
||||
```typescript
|
||||
// Пример: поставки расходников ФФ
|
||||
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
|
||||
where: {
|
||||
// Только свои поставки для ФФ
|
||||
fulfillmentCenterId: user.organizationId
|
||||
}
|
||||
})
|
||||
|
||||
// Фильтрация полей по роли
|
||||
return supplies.map(supply =>
|
||||
SupplyDataFilter.filterByRole(supply, user.organizationType)
|
||||
)
|
||||
```
|
||||
|
||||
### 📈 **Масштабируемость**
|
||||
|
||||
Новые типы поставок добавляются независимо:
|
||||
|
||||
```typescript
|
||||
// Будущее: Поставки на Яндекс.Маркет
|
||||
interface YandexMarketSupplyOrder {
|
||||
// специфичные поля для Яндекс.Маркета
|
||||
}
|
||||
|
||||
// Отдельные операции
|
||||
createYandexMarketSupply()
|
||||
myYandexMarketSupplies()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ ПРЕИМУЩЕСТВА НОВОЙ АРХИТЕКТУРЫ
|
||||
|
||||
### 🎯 **Четкое разделение ответственности**
|
||||
- Каждый тип поставки имеет свою логику
|
||||
- Независимая разработка и тестирование
|
||||
- Простота добавления новых типов
|
||||
|
||||
### 🔒 **Надежная безопасность**
|
||||
- Раздельные правила доступа для каждого типа
|
||||
- Невозможно случайно показать чужие данные
|
||||
- Гранулярные права по ролям
|
||||
|
||||
### 📈 **Масштабируемость**
|
||||
- Легко добавлять новые маркетплейсы
|
||||
- Независимые схемы для разных процессов
|
||||
- Оптимизация каждого типа отдельно
|
||||
|
||||
### 🛡️ **Безопасная миграция**
|
||||
- Поэтапное внедрение без остановки системы
|
||||
- Система откатов на каждом этапе
|
||||
- Сохранение работоспособности старой системы
|
||||
|
||||
---
|
||||
|
||||
## 🚀 ПЛАН ВНЕДРЕНИЯ
|
||||
|
||||
### **Phase 1:** FulfillmentConsumableSupplyOrder ⏳
|
||||
- Новая модель данных
|
||||
- GraphQL операции
|
||||
- Интерфейс создания и просмотра
|
||||
- Тестирование
|
||||
|
||||
### **Phase 2:** SellerConsumableSupplyOrder
|
||||
- Аналогично Phase 1
|
||||
- Интеграция с системой хранения
|
||||
|
||||
### **Phase 3:** GoodsSupplyOrder
|
||||
- Самый сложный тип с рецептурами
|
||||
- Миграция существующих товарных поставок
|
||||
|
||||
### **Phase 4:** Поставки на маркетплейсы
|
||||
- Отдельная система для Ozon/WB
|
||||
- API интеграции с маркетплейсами
|
||||
|
||||
### **Phase 5:** Очистка и оптимизация
|
||||
- Миграция старых данных
|
||||
- Удаление устаревшего кода (с одобрения)
|
||||
- Финальная оптимизация
|
||||
|
||||
**Следующий шаг:** Начало реализации Phase 1 - FulfillmentConsumableSupplyOrder
|
487
docs/business-processes/SUPPLY_SYSTEM_ARCHITECTURE.md
Normal file
487
docs/business-processes/SUPPLY_SYSTEM_ARCHITECTURE.md
Normal file
@ -0,0 +1,487 @@
|
||||
# 🚚 АРХИТЕКТУРА СИСТЕМЫ ПОСТАВОК SFERA
|
||||
|
||||
## 🎯 ОБЗОР СИСТЕМЫ
|
||||
|
||||
Система поставок SFERA включает **5 типов поставок**, разделенных на две категории:
|
||||
|
||||
### 📦 **ПОСТАВКИ НА ФУЛФИЛМЕНТ (3 типа)**
|
||||
1. **Товарные поставки** - товары селлера → склад ФФ
|
||||
2. **Поставки расходников ФФ** - расходники ФФ → склад ФФ
|
||||
3. **Поставки расходников селлеров** - расходники селлера → склад ФФ (на хранение)
|
||||
|
||||
### 🛒 **ПОСТАВКИ НА МАРКЕТПЛЕЙСЫ (2+ типов)**
|
||||
4. **Поставки на Ozon** - готовые продукты со склада ФФ → Ozon
|
||||
5. **Поставки на Wildberries** - готовые продукты со склада ФФ → Wildberries
|
||||
|
||||
---
|
||||
|
||||
## 📊 АРХИТЕКТУРА ДАННЫХ
|
||||
|
||||
### ✅ **ПРИНЦИП: ОТДЕЛЬНАЯ ТАБЛИЦА ДЛЯ КАЖДОГО ТИПА ПОСТАВКИ**
|
||||
|
||||
#### **Преимущества подхода:**
|
||||
- 🎯 Четкое разделение ответственности
|
||||
- 🔒 Упрощение системы безопасности
|
||||
- 📈 Независимые схемы для разных процессов
|
||||
- 🔧 Простота миграций и изменений
|
||||
- 📝 Специфичные поля для каждого типа
|
||||
|
||||
### 🏗️ **СТРУКТУРА ТАБЛИЦ ПОСТАВОК НА ФУЛФИЛМЕНТ**
|
||||
|
||||
#### **1. GoodsSupplyOrder - Товарные поставки**
|
||||
```typescript
|
||||
interface GoodsSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id: string
|
||||
status: SupplyOrderStatus
|
||||
createdAt: DateTime
|
||||
updatedAt: DateTime
|
||||
|
||||
// === ДАННЫЕ СЕЛЛЕРА (создатель) ===
|
||||
sellerId: string // кто заказывает
|
||||
fulfillmentCenterId: string // куда доставить
|
||||
requestedDeliveryDate: DateTime // когда нужно
|
||||
|
||||
// === ДАННЫЕ ПОСТАВЩИКА ===
|
||||
supplierId: string // кто поставляет
|
||||
supplierApprovedAt?: DateTime // когда одобрил
|
||||
packagesCount?: number // количество грузомест
|
||||
estimatedVolume?: number // объем груза
|
||||
supplierContractId?: string // номер договора
|
||||
|
||||
// === ДАННЫЕ ЛОГИСТИКИ ===
|
||||
logisticsPartnerId?: string // кто везет
|
||||
estimatedDeliveryDate?: DateTime // план доставки
|
||||
routeId?: string // маршрут
|
||||
logisticsCost?: number // стоимость доставки
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
shippedAt?: DateTime // факт отгрузки
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ ===
|
||||
receivedAt?: DateTime // факт приемки
|
||||
receivedById?: string // кто принял (сотрудник ФФ)
|
||||
actualQuantity?: number // принято количество
|
||||
defectQuantity?: number // брак
|
||||
|
||||
// === УНИКАЛЬНЫЕ ПОЛЯ ДЛЯ ТОВАРОВ ===
|
||||
hasRecipes: boolean // есть ли рецептуры
|
||||
totalServicesValue?: number // стоимость услуг ФФ
|
||||
|
||||
// === СВЯЗИ ===
|
||||
items: GoodsSupplyItem[]
|
||||
}
|
||||
|
||||
interface GoodsSupplyItem {
|
||||
id: string
|
||||
supplyOrderId: string // связь с поставкой
|
||||
productId: string // какой товар
|
||||
requestedQuantity: number // запросили
|
||||
approvedQuantity?: number // поставщик одобрил
|
||||
shippedQuantity?: number // отгрузили
|
||||
receivedQuantity?: number // приняли
|
||||
defectQuantity?: number // брак
|
||||
unitPrice: number // цена за единицу
|
||||
totalPrice: number // общая стоимость
|
||||
|
||||
// === РЕЦЕПТУРА (JSON) ===
|
||||
recipe?: {
|
||||
services: string[] // ID услуг ФФ
|
||||
fulfillmentConsumables: string[] // ID расходников ФФ
|
||||
sellerConsumables: string[] // ID расходников селлера
|
||||
marketplaceCardId?: string // карточка товара
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **2. FulfillmentConsumableSupplyOrder - Поставки расходников ФФ**
|
||||
```typescript
|
||||
interface FulfillmentConsumableSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id: string
|
||||
status: SupplyOrderStatus
|
||||
createdAt: DateTime
|
||||
updatedAt: DateTime
|
||||
|
||||
// === ДАННЫЕ ФФ (создатель) ===
|
||||
fulfillmentCenterId: string // кто заказывает
|
||||
requestedDeliveryDate: DateTime // когда нужно
|
||||
|
||||
// === ДАННЫЕ ПОСТАВЩИКА ===
|
||||
supplierId: string // кто поставляет
|
||||
supplierApprovedAt?: DateTime // когда одобрил
|
||||
packagesCount?: number // количество грузомест
|
||||
estimatedVolume?: number // объем груза
|
||||
supplierContractId?: string // номер договора
|
||||
|
||||
// === ДАННЫЕ ЛОГИСТИКИ ===
|
||||
logisticsPartnerId?: string // кто везет
|
||||
estimatedDeliveryDate?: DateTime // план доставки
|
||||
routeId?: string // маршрут
|
||||
logisticsCost?: number // стоимость доставки
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
shippedAt?: DateTime // факт отгрузки
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ ===
|
||||
receivedAt?: DateTime // факт приемки
|
||||
receivedById?: string // кто принял (сотрудник ФФ)
|
||||
actualQuantity?: number // принято количество
|
||||
defectQuantity?: number // брак
|
||||
|
||||
// === УНИКАЛЬНЫЕ ПОЛЯ ДЛЯ РАСХОДНИКОВ ФФ ===
|
||||
resalePricePerUnit?: number // цена продажи селлерам
|
||||
minStockLevel?: number // складские лимиты
|
||||
|
||||
// === СВЯЗИ ===
|
||||
items: FulfillmentConsumableSupplyItem[]
|
||||
}
|
||||
|
||||
interface FulfillmentConsumableSupplyItem {
|
||||
id: string
|
||||
supplyOrderId: string // связь с поставкой
|
||||
productId: string // какой расходник
|
||||
requestedQuantity: number // запросили
|
||||
approvedQuantity?: number // поставщик одобрил
|
||||
shippedQuantity?: number // отгрузили
|
||||
receivedQuantity?: number // приняли
|
||||
defectQuantity?: number // брак
|
||||
unitPrice: number // цена за единицу от поставщика
|
||||
totalPrice: number // общая стоимость
|
||||
}
|
||||
```
|
||||
|
||||
#### **3. SellerConsumableSupplyOrder - Поставки расходников селлеров**
|
||||
```typescript
|
||||
interface SellerConsumableSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id: string
|
||||
status: SupplyOrderStatus
|
||||
createdAt: DateTime
|
||||
updatedAt: DateTime
|
||||
|
||||
// === ДАННЫЕ СЕЛЛЕРА (создатель) ===
|
||||
sellerId: string // кто заказывает
|
||||
fulfillmentCenterId: string // где будет храниться
|
||||
requestedDeliveryDate: DateTime // когда нужно
|
||||
|
||||
// === ДАННЫЕ ПОСТАВЩИКА ===
|
||||
supplierId: string // кто поставляет
|
||||
supplierApprovedAt?: DateTime // когда одобрил
|
||||
packagesCount?: number // количество грузомест
|
||||
estimatedVolume?: number // объем груза
|
||||
supplierContractId?: string // номер договора
|
||||
|
||||
// === ДАННЫЕ ЛОГИСТИКИ ===
|
||||
logisticsPartnerId?: string // кто везет
|
||||
estimatedDeliveryDate?: DateTime // план доставки
|
||||
routeId?: string // маршрут
|
||||
logisticsCost?: number // стоимость доставки
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
shippedAt?: DateTime // факт отгрузки
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ ===
|
||||
receivedAt?: DateTime // факт приемки ФФ
|
||||
receivedById?: string // кто принял (сотрудник ФФ)
|
||||
actualQuantity?: number // принято количество
|
||||
defectQuantity?: number // брак
|
||||
|
||||
// === УНИКАЛЬНЫЕ ПОЛЯ ДЛЯ РАСХОДНИКОВ СЕЛЛЕРОВ ===
|
||||
storageTermMonths?: number // срок хранения
|
||||
accessRights: 'SELLER_ONLY' | 'SHARED_WITH_FF' // кто может использовать
|
||||
storageCostPerMonth?: number // стоимость хранения
|
||||
|
||||
// === СВЯЗИ ===
|
||||
items: SellerConsumableSupplyItem[]
|
||||
}
|
||||
|
||||
interface SellerConsumableSupplyItem {
|
||||
id: string
|
||||
supplyOrderId: string // связь с поставкой
|
||||
productId: string // какой расходник селлера
|
||||
requestedQuantity: number // запросили
|
||||
approvedQuantity?: number // поставщик одобрил
|
||||
shippedQuantity?: number // отгрузили
|
||||
receivedQuantity?: number // приняли
|
||||
defectQuantity?: number // брак
|
||||
unitPrice: number // цена за единицу
|
||||
totalPrice: number // общая стоимость
|
||||
}
|
||||
```
|
||||
|
||||
### 🛒 **СТРУКТУРА ТАБЛИЦ ПОСТАВОК НА МАРКЕТПЛЕЙСЫ**
|
||||
|
||||
#### **4. OzonSupplyOrder - Поставки на Ozon**
|
||||
```typescript
|
||||
interface OzonSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id: string
|
||||
status: MarketplaceSupplyStatus
|
||||
createdAt: DateTime
|
||||
updatedAt: DateTime
|
||||
|
||||
// === СПЕЦИФИЧНЫЕ ПОЛЯ OZON ===
|
||||
ozonWarehouseId: string // склад Ozon
|
||||
ozonSupplyId?: string // ID поставки в системе Ozon
|
||||
// ... дополнительные поля для Ozon API
|
||||
|
||||
// === СВЯЗИ ===
|
||||
items: OzonSupplyItem[]
|
||||
}
|
||||
```
|
||||
|
||||
#### **5. WildberriesSupplyOrder - Поставки на Wildberries**
|
||||
```typescript
|
||||
interface WildberriesSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id: string
|
||||
status: MarketplaceSupplyStatus
|
||||
createdAt: DateTime
|
||||
updatedAt: DateTime
|
||||
|
||||
// === СПЕЦИФИЧНЫЕ ПОЛЯ WB ===
|
||||
wbWarehouseId: string // склад WB
|
||||
wbSupplyId?: string // ID поставки в системе WB
|
||||
// ... дополнительные поля для WB API
|
||||
|
||||
// === СВЯЗИ ===
|
||||
items: WildberriesSupplyItem[]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 СИСТЕМА БЕЗОПАСНОСТИ
|
||||
|
||||
### 📋 **ПРАВИЛА ДОСТУПА К ПОСТАВКАМ РАСХОДНИКОВ ФФ**
|
||||
|
||||
**Кто видит и что:**
|
||||
- ✅ **ФФ (создатель):** все детали + цены + остатки
|
||||
- ✅ **Поставщик:** товары, количества, НЕ цены продажи ФФ
|
||||
- ✅ **Логистика:** маршруты, объемы, НЕ коммерческие данные
|
||||
- ❌ **Селлеры:** вообще не видят
|
||||
|
||||
### 📋 **ПРАВИЛА ДОСТУПА К ПОСТАВКАМ РАСХОДНИКОВ СЕЛЛЕРОВ**
|
||||
|
||||
**Кто видит и что:**
|
||||
- ✅ **Селлер (создатель):** все свои детали + свои цены
|
||||
- ✅ **ФФ (хранитель):** факт хранения + количества, НЕ закупочные цены селлера
|
||||
- ✅ **Поставщик:** товары, количества для селлера
|
||||
- ✅ **Логистика:** маршруты, объемы
|
||||
- ❌ **Другие селлеры:** не видят чужие расходники
|
||||
|
||||
### 📋 **ПРАВИЛА ДОСТУПА К ТОВАРНЫМ ПОСТАВКАМ**
|
||||
|
||||
**Кто видит и что:**
|
||||
- ✅ **Селлер (создатель):** все детали своих товаров + рецептуры
|
||||
- ✅ **ФФ (получатель):** товары + рецептуры + услуги, НЕ закупочные цены селлера
|
||||
- ✅ **Поставщик:** товары + количества, НЕ рецептуры и НЕ цены ФФ
|
||||
- ✅ **Логистика:** маршруты + объемы, НЕ коммерческие данные
|
||||
- ❌ **Другие селлеры:** не видят чужие поставки
|
||||
|
||||
---
|
||||
|
||||
## 🌐 URL-СТРУКТУРА ИНТЕРФЕЙСОВ
|
||||
|
||||
### 📦 **ПОСТАВКИ НА ФУЛФИЛМЕНТ**
|
||||
```
|
||||
/fulfillment-supplies/goods?status=new # Товар → Новые
|
||||
/fulfillment-supplies/goods?status=receiving # Товар → Приёмка
|
||||
/fulfillment-supplies/goods?status=accepted # Товар → Принято
|
||||
/fulfillment-supplies/ff-consumables # Расходники фулфилмента
|
||||
/fulfillment-supplies/seller-consumables # Расходники селлеров
|
||||
/fulfillment-supplies/create-consumables # Создание поставки расходников ФФ
|
||||
```
|
||||
|
||||
### 🛒 **ПОСТАВКИ НА МАРКЕТПЛЕЙСЫ**
|
||||
```
|
||||
/marketplace-supplies/ozon # Поставки на Ozon
|
||||
/marketplace-supplies/wildberries # Поставки на Wildberries
|
||||
/marketplace-supplies/create-ozon # Создание поставки на Ozon
|
||||
/marketplace-supplies/create-wildberries # Создание поставки на WB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ GRAPHQL API АРХИТЕКТУРА
|
||||
|
||||
### 🎯 **ПРИНЦИП: ОТДЕЛЬНЫЕ ОПЕРАЦИИ ДЛЯ КАЖДОГО ТИПА**
|
||||
|
||||
#### **Поставки расходников ФФ:**
|
||||
```graphql
|
||||
# Queries
|
||||
myFulfillmentConsumableSupplies: [FulfillmentConsumableSupplyOrder!]!
|
||||
fulfillmentConsumableSupply(id: ID!): FulfillmentConsumableSupplyOrder
|
||||
|
||||
# Mutations
|
||||
createFulfillmentConsumableSupply(input: CreateFulfillmentConsumableSupplyInput!): CreateSupplyResult!
|
||||
updateFulfillmentConsumableSupply(id: ID!, input: UpdateFulfillmentConsumableSupplyInput!): UpdateSupplyResult!
|
||||
```
|
||||
|
||||
#### **Поставки расходников селлеров:**
|
||||
```graphql
|
||||
# Queries
|
||||
mySellerConsumableSupplies: [SellerConsumableSupplyOrder!]!
|
||||
sellerConsumableSupply(id: ID!): SellerConsumableSupplyOrder
|
||||
|
||||
# Mutations
|
||||
createSellerConsumableSupply(input: CreateSellerConsumableSupplyInput!): CreateSupplyResult!
|
||||
updateSellerConsumableSupply(id: ID!, input: UpdateSellerConsumableSupplyInput!): UpdateSupplyResult!
|
||||
```
|
||||
|
||||
#### **Товарные поставки:**
|
||||
```graphql
|
||||
# Queries
|
||||
myGoodsSupplies: [GoodsSupplyOrder!]!
|
||||
goodsSupply(id: ID!): GoodsSupplyOrder
|
||||
|
||||
# Mutations
|
||||
createGoodsSupply(input: CreateGoodsSupplyInput!): CreateSupplyResult!
|
||||
updateGoodsSupply(id: ID!, input: UpdateGoodsSupplyInput!): UpdateSupplyResult!
|
||||
```
|
||||
|
||||
#### **Поставки на маркетплейсы (отдельная система):**
|
||||
```graphql
|
||||
# Queries
|
||||
myOzonSupplies: [OzonSupplyOrder!]!
|
||||
myWildberriesSupplies: [WildberriesSupplyOrder!]!
|
||||
|
||||
# Mutations
|
||||
createOzonSupply(input: CreateOzonSupplyInput!): CreateMarketplaceSupplyResult!
|
||||
createWildberriesSupply(input: CreateWildberriesSupplyInput!): CreateMarketplaceSupplyResult!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 ПЛАН ПОЭТАПНОЙ МИГРАЦИИ
|
||||
|
||||
### ✅ **ПРИНЦИПЫ БЕЗОПАСНОЙ МИГРАЦИИ**
|
||||
- 🛡️ Сохранение работоспособности существующей системы
|
||||
- 🔄 Система откатов на каждом этапе
|
||||
- 🧪 Тестирование каждого типа поставки отдельно
|
||||
- 📝 Модульная архитектура
|
||||
- ❌ **Удаление старого кода ТОЛЬКО после одобрения пользователя**
|
||||
|
||||
### **ЭТАП 1: FulfillmentConsumableSupplyOrder**
|
||||
|
||||
#### **1.1 Создание новой модели данных**
|
||||
```typescript
|
||||
// Новая таблица параллельно со старой
|
||||
model FulfillmentConsumableSupplyOrder {
|
||||
// ... вся структура
|
||||
}
|
||||
|
||||
// Старая таблица остается для совместимости
|
||||
model SupplyOrder {
|
||||
// ... существующая структура
|
||||
}
|
||||
```
|
||||
|
||||
#### **1.2 GraphQL API**
|
||||
```graphql
|
||||
# Новые операции
|
||||
createFulfillmentConsumableSupply()
|
||||
myFulfillmentConsumableSupplies()
|
||||
|
||||
# Старые операции остаются работать
|
||||
createSupplyOrder() # для других типов
|
||||
mySupplyOrders() # для других типов
|
||||
```
|
||||
|
||||
#### **1.3 Интерфейс**
|
||||
- Форма `/fulfillment-supplies/create-consumables` → новая мутация
|
||||
- Вкладка `/fulfillment-supplies/ff-consumables` → новый запрос
|
||||
|
||||
#### **1.4 Тестирование**
|
||||
- ✅ Создание поставки расходников ФФ
|
||||
- ✅ Отображение в интерфейсе
|
||||
- ✅ Безопасность доступа
|
||||
- ✅ Работа старой системы
|
||||
|
||||
### **ЭТАП 2: SellerConsumableSupplyOrder**
|
||||
- Аналогично этапу 1
|
||||
- Тестирование + интеграция
|
||||
|
||||
### **ЭТАП 3: GoodsSupplyOrder**
|
||||
- Аналогично этапу 1
|
||||
- Тестирование + интеграция
|
||||
|
||||
### **ЭТАП 4: Очистка (ТОЛЬКО после одобрения)**
|
||||
- Миграция данных из старых таблиц
|
||||
- Удаление старого кода
|
||||
- Обновление документации
|
||||
|
||||
---
|
||||
|
||||
## 🎯 СТАТУСЫ ПОСТАВОК
|
||||
|
||||
### **SupplyOrderStatus (для поставок НА фулфилмент)**
|
||||
```typescript
|
||||
enum SupplyOrderStatus {
|
||||
PENDING // Создана, ждет одобрения поставщика
|
||||
SUPPLIER_APPROVED // Одобрена поставщиком
|
||||
LOGISTICS_CONFIRMED // Логистика подтверждена
|
||||
SHIPPED // Отгружена поставщиком
|
||||
IN_TRANSIT // В пути
|
||||
DELIVERED // Доставлена на склад ФФ
|
||||
CANCELLED // Отменена
|
||||
}
|
||||
```
|
||||
|
||||
### **MarketplaceSupplyStatus (для поставок НА маркетплейсы)**
|
||||
```typescript
|
||||
enum MarketplaceSupplyStatus {
|
||||
PLANNED // Запланирована
|
||||
PREPARED // Подготовлена к отгрузке
|
||||
SHIPPED_TO_MARKETPLACE // Отгружена на маркетплейс
|
||||
ACCEPTED_BY_MARKETPLACE // Принята маркетплейсом
|
||||
CANCELLED // Отменена
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 ИНТЕГРАЦИИ
|
||||
|
||||
### **Поставки НА фулфилмент:**
|
||||
- 🏪 **DaData API** - валидация ИНН поставщиков
|
||||
- 📱 **SMS Aero** - уведомления участникам
|
||||
- ☁️ **AWS S3** - документы поставок
|
||||
- 🚚 **Логистические партнеры** - трекинг доставки
|
||||
|
||||
### **Поставки НА маркетплейсы:**
|
||||
- 🛒 **Ozon API** - создание поставок
|
||||
- 🛍️ **Wildberries API** - создание поставок
|
||||
- 📦 **Трекинг системы** - статусы доставки
|
||||
- 💰 **Биллинг системы** - расчет комиссий
|
||||
|
||||
---
|
||||
|
||||
## 🔍 МОНИТОРИНГ И АУДИТ
|
||||
|
||||
### **Коммерческие данные:**
|
||||
- 📊 Логирование доступа к ценам
|
||||
- 🔐 Аудит прав доступа по ролям
|
||||
- 📈 Метрики безопасности
|
||||
- ⚠️ Алерты на подозрительную активность
|
||||
|
||||
### **Операционные метрики:**
|
||||
- ⏱️ Время выполнения поставок
|
||||
- 📦 Процент успешных доставок
|
||||
- 🔄 SLA по статусам
|
||||
- 📊 Аналитика по типам поставок
|
||||
|
||||
---
|
||||
|
||||
## ✅ ЗАКЛЮЧЕНИЕ
|
||||
|
||||
Новая архитектура системы поставок обеспечивает:
|
||||
|
||||
- 🎯 **Четкое разделение** типов поставок
|
||||
- 🔒 **Надежную безопасность** коммерческих данных
|
||||
- 📈 **Масштабируемость** для новых маркетплейсов
|
||||
- 🔧 **Простоту развития** каждого типа независимо
|
||||
- 🛡️ **Безопасную миграцию** без потери данных
|
||||
|
||||
**Следующий шаг:** Поэтапная реализация начиная с `FulfillmentConsumableSupplyOrder`
|
600
docs/development/DATABASE_SCHEMA_V2.md
Normal file
600
docs/development/DATABASE_SCHEMA_V2.md
Normal file
@ -0,0 +1,600 @@
|
||||
# 🗄️ СХЕМА БАЗЫ ДАННЫХ SFERA v2.0 - СИСТЕМА ПОСТАВОК
|
||||
|
||||
> **⚠️ ВАЖНО:** Этот документ описывает НОВЫЕ таблицы для системы поставок v2.0. Существующие таблицы остаются без изменений для обратной совместимости.
|
||||
|
||||
## 📦 НОВЫЕ ТАБЛИЦЫ ПОСТАВОК НА ФУЛФИЛМЕНТ
|
||||
|
||||
### 1️⃣ **FulfillmentConsumableSupplyOrder - Поставки расходников ФФ**
|
||||
|
||||
```prisma
|
||||
model FulfillmentConsumableSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id String @id @default(cuid())
|
||||
status SupplyOrderStatusV2 @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === ДАННЫЕ ФФ (создатель) ===
|
||||
fulfillmentCenterId String // кто заказывает (FK: Organization)
|
||||
requestedDeliveryDate DateTime // когда нужно
|
||||
resalePricePerUnit Decimal? @db.Decimal(10, 2) // цена продажи селлерам
|
||||
minStockLevel Int? // минимальный остаток
|
||||
notes String? // заметки ФФ
|
||||
|
||||
// === ДАННЫЕ ПОСТАВЩИКА ===
|
||||
supplierId String? // кто поставляет (FK: Organization)
|
||||
supplierApprovedAt DateTime? // когда одобрил
|
||||
packagesCount Int? // количество грузомест
|
||||
estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³
|
||||
supplierContractId String? // номер договора
|
||||
supplierNotes String? // заметки поставщика
|
||||
|
||||
// === ДАННЫЕ ЛОГИСТИКИ ===
|
||||
logisticsPartnerId String? // кто везет (FK: Organization)
|
||||
estimatedDeliveryDate DateTime? // план доставки
|
||||
routeId String? // маршрут (FK: LogisticsRoute)
|
||||
logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки
|
||||
logisticsNotes String? // заметки логистики
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
shippedAt DateTime? // факт отгрузки
|
||||
trackingNumber String? // номер отслеживания
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ ===
|
||||
receivedAt DateTime? // факт приемки
|
||||
receivedById String? // кто принял (FK: User)
|
||||
actualQuantity Int? // принято количество
|
||||
defectQuantity Int? // брак
|
||||
receiptNotes String? // заметки приемки
|
||||
|
||||
// === СВЯЗИ ===
|
||||
fulfillmentCenter Organization @relation("FFSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id])
|
||||
supplier Organization? @relation("FFSupplyOrdersSupplier", fields: [supplierId], references: [id])
|
||||
logisticsPartner Organization? @relation("FFSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id])
|
||||
receivedBy User? @relation("FFSupplyOrdersReceiver", fields: [receivedById], references: [id])
|
||||
items FulfillmentConsumableSupplyItem[]
|
||||
|
||||
@@map("fulfillment_consumable_supply_orders")
|
||||
}
|
||||
|
||||
model FulfillmentConsumableSupplyItem {
|
||||
id String @id @default(cuid())
|
||||
supplyOrderId String // связь с поставкой
|
||||
productId String // какой расходник (FK: Product)
|
||||
|
||||
// === КОЛИЧЕСТВА ===
|
||||
requestedQuantity Int // запросили
|
||||
approvedQuantity Int? // поставщик одобрил
|
||||
shippedQuantity Int? // отгрузили
|
||||
receivedQuantity Int? // приняли
|
||||
defectQuantity Int? @default(0) // брак
|
||||
|
||||
// === ЦЕНЫ ===
|
||||
unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика
|
||||
totalPrice Decimal @db.Decimal(12, 2) // общая стоимость
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === СВЯЗИ ===
|
||||
supplyOrder FulfillmentConsumableSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation("FFSupplyItems", fields: [productId], references: [id])
|
||||
|
||||
@@unique([supplyOrderId, productId])
|
||||
@@map("fulfillment_consumable_supply_items")
|
||||
}
|
||||
```
|
||||
|
||||
### 2️⃣ **SellerConsumableSupplyOrder - Поставки расходников селлеров**
|
||||
|
||||
```prisma
|
||||
model SellerConsumableSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id String @id @default(cuid())
|
||||
status SupplyOrderStatusV2 @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === ДАННЫЕ СЕЛЛЕРА (создатель) ===
|
||||
sellerId String // кто заказывает (FK: Organization)
|
||||
fulfillmentCenterId String // где будет храниться (FK: Organization)
|
||||
requestedDeliveryDate DateTime // когда нужно
|
||||
notes String? // заметки селлера
|
||||
|
||||
// === ДАННЫЕ ПОСТАВЩИКА ===
|
||||
supplierId String? // кто поставляет (FK: Organization)
|
||||
supplierApprovedAt DateTime? // когда одобрил
|
||||
packagesCount Int? // количество грузомест
|
||||
estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³
|
||||
supplierContractId String? // номер договора
|
||||
supplierNotes String? // заметки поставщика
|
||||
|
||||
// === ДАННЫЕ ЛОГИСТИКИ ===
|
||||
logisticsPartnerId String? // кто везет (FK: Organization)
|
||||
estimatedDeliveryDate DateTime? // план доставки
|
||||
routeId String? // маршрут (FK: LogisticsRoute)
|
||||
logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки
|
||||
logisticsNotes String? // заметки логистики
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
shippedAt DateTime? // факт отгрузки
|
||||
trackingNumber String? // номер отслеживания
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ ===
|
||||
receivedAt DateTime? // факт приемки ФФ
|
||||
receivedById String? // кто принял (FK: User)
|
||||
actualQuantity Int? // принято количество
|
||||
defectQuantity Int? // брак
|
||||
receiptNotes String? // заметки приемки
|
||||
|
||||
// === УНИКАЛЬНЫЕ ПОЛЯ ДЛЯ РАСХОДНИКОВ СЕЛЛЕРОВ ===
|
||||
storageTermMonths Int? // срок хранения в месяцах
|
||||
accessRights SellerConsumableAccessRights @default(SELLER_ONLY)
|
||||
storageCostPerMonth Decimal? @db.Decimal(8, 2) // стоимость хранения за месяц
|
||||
|
||||
// === СВЯЗИ ===
|
||||
seller Organization @relation("SellerSupplyOrdersSeller", fields: [sellerId], references: [id])
|
||||
fulfillmentCenter Organization @relation("SellerSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id])
|
||||
supplier Organization? @relation("SellerSupplyOrdersSupplier", fields: [supplierId], references: [id])
|
||||
logisticsPartner Organization? @relation("SellerSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id])
|
||||
receivedBy User? @relation("SellerSupplyOrdersReceiver", fields: [receivedById], references: [id])
|
||||
items SellerConsumableSupplyItem[]
|
||||
|
||||
@@map("seller_consumable_supply_orders")
|
||||
}
|
||||
|
||||
model SellerConsumableSupplyItem {
|
||||
id String @id @default(cuid())
|
||||
supplyOrderId String // связь с поставкой
|
||||
productId String // какой расходник селлера (FK: Product)
|
||||
|
||||
// === КОЛИЧЕСТВА ===
|
||||
requestedQuantity Int // запросили
|
||||
approvedQuantity Int? // поставщик одобрил
|
||||
shippedQuantity Int? // отгрузили
|
||||
receivedQuantity Int? // приняли
|
||||
defectQuantity Int? @default(0) // брак
|
||||
|
||||
// === ЦЕНЫ (видит только селлер) ===
|
||||
unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика
|
||||
totalPrice Decimal @db.Decimal(12, 2) // общая стоимость
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === СВЯЗИ ===
|
||||
supplyOrder SellerConsumableSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation("SellerSupplyItems", fields: [productId], references: [id])
|
||||
|
||||
@@unique([supplyOrderId, productId])
|
||||
@@map("seller_consumable_supply_items")
|
||||
}
|
||||
```
|
||||
|
||||
### 3️⃣ **GoodsSupplyOrder - Товарные поставки**
|
||||
|
||||
```prisma
|
||||
model GoodsSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id String @id @default(cuid())
|
||||
status SupplyOrderStatusV2 @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === ДАННЫЕ СЕЛЛЕРА (создатель) ===
|
||||
sellerId String // кто заказывает (FK: Organization)
|
||||
fulfillmentCenterId String // куда доставить (FK: Organization)
|
||||
requestedDeliveryDate DateTime // когда нужно
|
||||
notes String? // заметки селлера
|
||||
|
||||
// === ДАННЫЕ ПОСТАВЩИКА ===
|
||||
supplierId String? // кто поставляет (FK: Organization)
|
||||
supplierApprovedAt DateTime? // когда одобрил
|
||||
packagesCount Int? // количество грузомест
|
||||
estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³
|
||||
supplierContractId String? // номер договора
|
||||
supplierNotes String? // заметки поставщика
|
||||
|
||||
// === ДАННЫЕ ЛОГИСТИКИ ===
|
||||
logisticsPartnerId String? // кто везет (FK: Organization)
|
||||
estimatedDeliveryDate DateTime? // план доставки
|
||||
routeId String? // маршрут (FK: LogisticsRoute)
|
||||
logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки
|
||||
logisticsNotes String? // заметки логистики
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
shippedAt DateTime? // факт отгрузки
|
||||
trackingNumber String? // номер отслеживания
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ ===
|
||||
receivedAt DateTime? // факт приемки
|
||||
receivedById String? // кто принял (FK: User)
|
||||
actualQuantity Int? // принято количество
|
||||
defectQuantity Int? // брак
|
||||
receiptNotes String? // заметки приемки
|
||||
|
||||
// === УНИКАЛЬНЫЕ ПОЛЯ ДЛЯ ТОВАРОВ ===
|
||||
hasRecipes Boolean @default(false) // есть ли рецептуры
|
||||
totalServicesValue Decimal? @db.Decimal(12, 2) // общая стоимость услуг ФФ
|
||||
|
||||
// === СВЯЗИ ===
|
||||
seller Organization @relation("GoodsSupplyOrdersSeller", fields: [sellerId], references: [id])
|
||||
fulfillmentCenter Organization @relation("GoodsSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id])
|
||||
supplier Organization? @relation("GoodsSupplyOrdersSupplier", fields: [supplierId], references: [id])
|
||||
logisticsPartner Organization? @relation("GoodsSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id])
|
||||
receivedBy User? @relation("GoodsSupplyOrdersReceiver", fields: [receivedById], references: [id])
|
||||
items GoodsSupplyItem[]
|
||||
|
||||
@@map("goods_supply_orders")
|
||||
}
|
||||
|
||||
model GoodsSupplyItem {
|
||||
id String @id @default(cuid())
|
||||
supplyOrderId String // связь с поставкой
|
||||
productId String // какой товар (FK: Product)
|
||||
|
||||
// === КОЛИЧЕСТВА ===
|
||||
requestedQuantity Int // запросили
|
||||
approvedQuantity Int? // поставщик одобрил
|
||||
shippedQuantity Int? // отгрузили
|
||||
receivedQuantity Int? // приняли
|
||||
defectQuantity Int? @default(0) // брак
|
||||
|
||||
// === ЦЕНЫ (видит только селлер) ===
|
||||
unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика
|
||||
totalPrice Decimal @db.Decimal(12, 2) // общая стоимость
|
||||
|
||||
// === РЕЦЕПТУРА (JSON) ===
|
||||
recipe Json? // полная рецептура в JSON
|
||||
/*
|
||||
recipe structure:
|
||||
{
|
||||
services: string[] // ID услуг ФФ
|
||||
fulfillmentConsumables: string[] // ID расходников ФФ
|
||||
sellerConsumables: string[] // ID расходников селлера
|
||||
marketplaceCardId?: string // карточка товара
|
||||
}
|
||||
*/
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === СВЯЗИ ===
|
||||
supplyOrder GoodsSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation("GoodsSupplyItems", fields: [productId], references: [id])
|
||||
|
||||
@@unique([supplyOrderId, productId])
|
||||
@@map("goods_supply_items")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛒 ТАБЛИЦЫ ПОСТАВОК НА МАРКЕТПЛЕЙСЫ
|
||||
|
||||
### 4️⃣ **OzonSupplyOrder - Поставки на Ozon**
|
||||
|
||||
```prisma
|
||||
model OzonSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id String @id @default(cuid())
|
||||
status MarketplaceSupplyStatus @default(PLANNED)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === ДАННЫЕ ФФ (создатель) ===
|
||||
fulfillmentCenterId String // кто отгружает (FK: Organization)
|
||||
plannedShipmentDate DateTime // план отгрузки
|
||||
notes String? // заметки ФФ
|
||||
|
||||
// === СПЕЦИФИЧНЫЕ ПОЛЯ OZON ===
|
||||
ozonWarehouseId String // склад Ozon
|
||||
ozonSupplyId String? // ID поставки в системе Ozon
|
||||
ozonPostingNumber String? // номер отправления Ozon
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
preparedAt DateTime? // готово к отгрузке
|
||||
shippedAt DateTime? // отгружено
|
||||
trackingNumber String? // номер отслеживания
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ МАРКЕТПЛЕЙСОМ ===
|
||||
acceptedAt DateTime? // принято Ozon
|
||||
acceptedQuantity Int? // принято количество
|
||||
rejectedQuantity Int? // отклонено
|
||||
rejectionReason String? // причина отклонения
|
||||
|
||||
// === СВЯЗИ ===
|
||||
fulfillmentCenter Organization @relation("OzonSupplyOrders", fields: [fulfillmentCenterId], references: [id])
|
||||
items OzonSupplyItem[]
|
||||
|
||||
@@map("ozon_supply_orders")
|
||||
}
|
||||
|
||||
model OzonSupplyItem {
|
||||
id String @id @default(cuid())
|
||||
supplyOrderId String // связь с поставкой
|
||||
productId String // какой готовый продукт (FK: Product)
|
||||
|
||||
// === КОЛИЧЕСТВА ===
|
||||
plannedQuantity Int // планируется отгрузить
|
||||
preparedQuantity Int? // подготовлено
|
||||
shippedQuantity Int? // отгружено
|
||||
acceptedQuantity Int? // принято Ozon
|
||||
rejectedQuantity Int? @default(0) // отклонено
|
||||
|
||||
// === OZON СПЕЦИФИЧНЫЕ ПОЛЯ ===
|
||||
ozonProductId String? // ID товара в Ozon
|
||||
ozonSku String? // SKU в Ozon
|
||||
ozonBarcode String? // штрихкод
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === СВЯЗИ ===
|
||||
supplyOrder OzonSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation("OzonSupplyItems", fields: [productId], references: [id])
|
||||
|
||||
@@unique([supplyOrderId, productId])
|
||||
@@map("ozon_supply_items")
|
||||
}
|
||||
```
|
||||
|
||||
### 5️⃣ **WildberriesSupplyOrder - Поставки на Wildberries**
|
||||
|
||||
```prisma
|
||||
model WildberriesSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id String @id @default(cuid())
|
||||
status MarketplaceSupplyStatus @default(PLANNED)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === ДАННЫЕ ФФ (создатель) ===
|
||||
fulfillmentCenterId String // кто отгружает (FK: Organization)
|
||||
plannedShipmentDate DateTime // план отгрузки
|
||||
notes String? // заметки ФФ
|
||||
|
||||
// === СПЕЦИФИЧНЫЕ ПОЛЯ WILDBERRIES ===
|
||||
wbWarehouseId String // склад WB
|
||||
wbSupplyId String? // ID поставки в системе WB
|
||||
wbStickerId String? // ID стикера WB
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
preparedAt DateTime? // готово к отгрузке
|
||||
shippedAt DateTime? // отгружено
|
||||
trackingNumber String? // номер отслеживания
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ МАРКЕТПЛЕЙСОМ ===
|
||||
acceptedAt DateTime? // принято WB
|
||||
acceptedQuantity Int? // принято количество
|
||||
rejectedQuantity Int? // отклонено
|
||||
rejectionReason String? // причина отклонения
|
||||
|
||||
// === СВЯЗИ ===
|
||||
fulfillmentCenter Organization @relation("WildberriesSupplyOrders", fields: [fulfillmentCenterId], references: [id])
|
||||
items WildberriesSupplyItem[]
|
||||
|
||||
@@map("wildberries_supply_orders")
|
||||
}
|
||||
|
||||
model WildberriesSupplyItem {
|
||||
id String @id @default(cuid())
|
||||
supplyOrderId String // связь с поставкой
|
||||
productId String // какой готовый продукт (FK: Product)
|
||||
|
||||
// === КОЛИЧЕСТВА ===
|
||||
plannedQuantity Int // планируется отгрузить
|
||||
preparedQuantity Int? // подготовлено
|
||||
shippedQuantity Int? // отгружено
|
||||
acceptedQuantity Int? // принято WB
|
||||
rejectedQuantity Int? @default(0) // отклонено
|
||||
|
||||
// === WB СПЕЦИФИЧНЫЕ ПОЛЯ ===
|
||||
wbNmId String? // Номенклатура WB
|
||||
wbSku String? // SKU в WB
|
||||
wbBarcode String? // штрихкод
|
||||
wbSize String? // размер для WB
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === СВЯЗИ ===
|
||||
supplyOrder WildberriesSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation("WBSupplyItems", fields: [productId], references: [id])
|
||||
|
||||
@@unique([supplyOrderId, productId])
|
||||
@@map("wildberries_supply_items")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 НОВЫЕ ENUMS
|
||||
|
||||
### **SupplyOrderStatusV2 - Статусы поставок НА фулфилмент**
|
||||
|
||||
```prisma
|
||||
enum SupplyOrderStatusV2 {
|
||||
PENDING // Ожидает одобрения поставщика
|
||||
SUPPLIER_APPROVED // Одобрено поставщиком
|
||||
LOGISTICS_CONFIRMED // Логистика подтверждена
|
||||
SHIPPED // Отгружено поставщиком
|
||||
IN_TRANSIT // В пути
|
||||
DELIVERED // Доставлено и принято
|
||||
CANCELLED // Отменено
|
||||
}
|
||||
```
|
||||
|
||||
### **MarketplaceSupplyStatus - Статусы поставок НА маркетплейсы**
|
||||
|
||||
```prisma
|
||||
enum MarketplaceSupplyStatus {
|
||||
PLANNED // Запланирована
|
||||
PREPARED // Подготовлена к отгрузке
|
||||
SHIPPED_TO_MARKETPLACE // Отгружена на маркетплейс
|
||||
ACCEPTED_BY_MARKETPLACE // Принята маркетплейсом
|
||||
CANCELLED // Отменена
|
||||
}
|
||||
```
|
||||
|
||||
### **SellerConsumableAccessRights - Права доступа к расходникам селлеров**
|
||||
|
||||
```prisma
|
||||
enum SellerConsumableAccessRights {
|
||||
SELLER_ONLY // Только селлер может использовать
|
||||
SHARED_WITH_FF // ФФ тоже может использовать (с разрешения)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 ОБНОВЛЕНИЯ СУЩЕСТВУЮЩИХ ТАБЛИЦ
|
||||
|
||||
### **Product - Добавление новых связей**
|
||||
|
||||
```prisma
|
||||
model Product {
|
||||
// ... существующие поля остаются без изменений
|
||||
|
||||
// === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 ===
|
||||
fulfillmentSupplyItems FulfillmentConsumableSupplyItem[] @relation("FFSupplyItems")
|
||||
sellerSupplyItems SellerConsumableSupplyItem[] @relation("SellerSupplyItems")
|
||||
goodsSupplyItems GoodsSupplyItem[] @relation("GoodsSupplyItems")
|
||||
ozonSupplyItems OzonSupplyItem[] @relation("OzonSupplyItems")
|
||||
wildberriesSupplyItems WildberriesSupplyItem[] @relation("WBSupplyItems")
|
||||
|
||||
// ... остальные поля остаются без изменений
|
||||
}
|
||||
```
|
||||
|
||||
### **Organization - Добавление новых связей**
|
||||
|
||||
```prisma
|
||||
model Organization {
|
||||
// ... существующие поля остаются без изменений
|
||||
|
||||
// === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 ===
|
||||
// Поставки расходников ФФ
|
||||
fulfillmentSupplyOrdersAsFulfillment FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersFulfillment")
|
||||
fulfillmentSupplyOrdersAsSupplier FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersSupplier")
|
||||
fulfillmentSupplyOrdersAsLogistics FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersLogistics")
|
||||
|
||||
// Поставки расходников селлеров
|
||||
sellerSupplyOrdersAsSeller SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersSeller")
|
||||
sellerSupplyOrdersAsFulfillment SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersFulfillment")
|
||||
sellerSupplyOrdersAsSupplier SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersSupplier")
|
||||
sellerSupplyOrdersAsLogistics SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersLogistics")
|
||||
|
||||
// Товарные поставки
|
||||
goodsSupplyOrdersAsSeller GoodsSupplyOrder[] @relation("GoodsSupplyOrdersSeller")
|
||||
goodsSupplyOrdersAsFulfillment GoodsSupplyOrder[] @relation("GoodsSupplyOrdersFulfillment")
|
||||
goodsSupplyOrdersAsSupplier GoodsSupplyOrder[] @relation("GoodsSupplyOrdersSupplier")
|
||||
goodsSupplyOrdersAsLogistics GoodsSupplyOrder[] @relation("GoodsSupplyOrdersLogistics")
|
||||
|
||||
// Поставки на маркетплейсы
|
||||
ozonSupplyOrders OzonSupplyOrder[] @relation("OzonSupplyOrders")
|
||||
wildberriesSupplyOrders WildberriesSupplyOrder[] @relation("WildberriesSupplyOrders")
|
||||
|
||||
// ... остальные поля остаются без изменений
|
||||
}
|
||||
```
|
||||
|
||||
### **User - Добавление новых связей**
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
// ... существующие поля остаются без изменений
|
||||
|
||||
// === НОВЫЕ СВЯЗИ С ПРИЕМКОЙ ПОСТАВОК V2 ===
|
||||
fulfillmentSupplyOrdersReceived FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersReceiver")
|
||||
sellerSupplyOrdersReceived SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersReceiver")
|
||||
goodsSupplyOrdersReceived GoodsSupplyOrder[] @relation("GoodsSupplyOrdersReceiver")
|
||||
|
||||
// ... остальные поля остаются без изменений
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 ИНДЕКСЫ ДЛЯ ПРОИЗВОДИТЕЛЬНОСТИ
|
||||
|
||||
```prisma
|
||||
// Индексы для быстрого поиска поставок по статусу и организации
|
||||
@@index([fulfillmentCenterId, status])
|
||||
@@index([sellerId, status])
|
||||
@@index([supplierId, status])
|
||||
@@index([logisticsPartnerId, status])
|
||||
|
||||
// Индексы для поиска по датам
|
||||
@@index([createdAt])
|
||||
@@index([requestedDeliveryDate])
|
||||
@@index([estimatedDeliveryDate])
|
||||
|
||||
// Индексы для отслеживания
|
||||
@@index([trackingNumber])
|
||||
@@index([supplierContractId])
|
||||
|
||||
// Маркетплейс-специфичные индексы
|
||||
@@index([ozonSupplyId])
|
||||
@@index([ozonProductId])
|
||||
@@index([wbSupplyId])
|
||||
@@index([wbNmId])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 ПЛАН МИГРАЦИИ
|
||||
|
||||
### **Phase 1: Создание новых таблиц**
|
||||
```sql
|
||||
-- Создание таблиц параллельно с существующими
|
||||
CREATE TABLE fulfillment_consumable_supply_orders (...);
|
||||
CREATE TABLE fulfillment_consumable_supply_items (...);
|
||||
-- и т.д.
|
||||
```
|
||||
|
||||
### **Phase 2: Новые enum значения**
|
||||
```sql
|
||||
-- Добавление новых enum без затрагивания старых
|
||||
CREATE TYPE "SupplyOrderStatusV2" AS ENUM (...);
|
||||
CREATE TYPE "MarketplaceSupplyStatus" AS ENUM (...);
|
||||
```
|
||||
|
||||
### **Phase 3: Обновление связей (без удаления старых)**
|
||||
```sql
|
||||
-- Добавление новых foreign key связей
|
||||
ALTER TABLE "Product" ADD COLUMN ...;
|
||||
ALTER TABLE "Organization" ADD COLUMN ...;
|
||||
```
|
||||
|
||||
### **Phase 4: Данные миграции (только с одобрения)**
|
||||
```sql
|
||||
-- Миграция данных из старых таблиц в новые
|
||||
-- ТОЛЬКО после полного тестирования новой системы
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ВАЖНЫЕ ПРИНЦИПЫ
|
||||
|
||||
### ✅ **ОБРАТНАЯ СОВМЕСТИМОСТЬ**
|
||||
- Все существующие таблицы остаются без изменений
|
||||
- Новые таблицы создаются параллельно
|
||||
- Старая система продолжает работать
|
||||
|
||||
### 🔒 **БЕЗОПАСНОСТЬ ДАННЫХ**
|
||||
- Каждый тип поставки изолирован в отдельной таблице
|
||||
- Связи защищены foreign key constraints
|
||||
- Каскадное удаление только для зависимых записей
|
||||
|
||||
### 📈 **МАСШТАБИРУЕМОСТЬ**
|
||||
- Легко добавлять новые типы поставок
|
||||
- Оптимизированные индексы для каждого use case
|
||||
- Независимые схемы для разных процессов
|
||||
|
||||
### 🛡️ **ЦЕЛОСТНОСТЬ ДАННЫХ**
|
||||
- Строгие ограничения на связи между таблицами
|
||||
- Валидация через enum значения
|
||||
- Уникальные индексы предотвращают дублирование
|
||||
|
||||
**Следующий шаг:** Создание Prisma миграций для FulfillmentConsumableSupplyOrder
|
817
docs/development/SUPPLY_SYSTEM_IMPLEMENTATION_PLAN.md
Normal file
817
docs/development/SUPPLY_SYSTEM_IMPLEMENTATION_PLAN.md
Normal file
@ -0,0 +1,817 @@
|
||||
# 🛡️ ДЕТАЛЬНЫЙ БЕЗОПАСНЫЙ ПЛАН РЕАЛИЗАЦИИ СИСТЕМЫ ПОСТАВОК v2.0
|
||||
|
||||
> **🎯 ЦЕЛЬ:** Поэтапная миграция на новую архитектуру системы поставок без нарушения работы существующей системы
|
||||
|
||||
## 🔒 ПРИНЦИПЫ БЕЗОПАСНОЙ РЕАЛИЗАЦИИ
|
||||
|
||||
### ✅ **ЗОЛОТЫЕ ПРАВИЛА:**
|
||||
1. **НИКОГДА НЕ ЛОМАТЬ СУЩЕСТВУЮЩИЙ ФУНКЦИОНАЛ** - старая система работает на 100%
|
||||
2. **КАЖДЫЙ ЭТАП ИМЕЕТ ТОЧКУ ОТКАТА** - можно вернуться к предыдущему состоянию
|
||||
3. **ТЕСТИРОВАНИЕ ПЕРЕД ПРОДАКШЕНОМ** - каждое изменение проверяется отдельно
|
||||
4. **МОДУЛЬНОСТЬ** - изменения изолированы друг от друга
|
||||
5. **ОДОБРЕНИЕ ПОЛЬЗОВАТЕЛЯ** - удаление старого кода ТОЛЬКО с разрешения
|
||||
6. **МОНИТОРИНГ** - отслеживание работы на каждом этапе
|
||||
|
||||
### 🛠️ **ИНСТРУМЕНТЫ БЕЗОПАСНОСТИ:**
|
||||
- **Feature Flags** - включение/отключение новой функциональности
|
||||
- **Database Migrations** - обратимые изменения схемы БД
|
||||
- **Parallel Testing** - новая система тестируется параллельно со старой
|
||||
- **Gradual Rollout** - постепенное включение для пользователей
|
||||
- **Automated Rollback** - автоматический откат при критических ошибках
|
||||
|
||||
---
|
||||
|
||||
## 📋 PHASE 1: FULFILLMENT CONSUMABLE SUPPLY ORDERS
|
||||
|
||||
> **Сроки:** 2-3 недели
|
||||
> **Риск:** НИЗКИЙ (новая функциональность параллельно со старой)
|
||||
|
||||
### 🎯 **ЦЕЛЬ PHASE 1:**
|
||||
Создать полностью рабочую систему для поставок расходников фулфилмента, которая работает параллельно со старой системой.
|
||||
|
||||
---
|
||||
|
||||
## 📊 STEP 1.1: ПОДГОТОВКА ИНФРАСТРУКТУРЫ
|
||||
|
||||
### **Задачи:**
|
||||
1. ✅ Создание feature flag системы
|
||||
2. ✅ Настройка параллельного тестирования
|
||||
3. ✅ Подготовка инструментов мониторинга
|
||||
|
||||
### **1.1.1 Feature Flags**
|
||||
```typescript
|
||||
// /src/lib/featureFlags.ts
|
||||
export const FEATURE_FLAGS = {
|
||||
FULFILLMENT_CONSUMABLE_SUPPLY_V2: process.env.FULFILLMENT_CONSUMABLE_V2 === 'true',
|
||||
ENABLE_PARALLEL_TESTING: process.env.PARALLEL_TESTING === 'true',
|
||||
} as const
|
||||
|
||||
export function isFeatureEnabled(flag: keyof typeof FEATURE_FLAGS): boolean {
|
||||
return FEATURE_FLAGS[flag] || false
|
||||
}
|
||||
```
|
||||
|
||||
### **1.1.2 Мониторинг**
|
||||
```typescript
|
||||
// /src/lib/monitoring/supplySystemMonitor.ts
|
||||
export class SupplySystemMonitor {
|
||||
static logMigrationEvent(event: string, data: any) {
|
||||
console.log(`[SUPPLY_V2_MIGRATION] ${event}:`, data)
|
||||
// Отправка в систему мониторинга
|
||||
}
|
||||
|
||||
static logError(error: Error, context: string) {
|
||||
console.error(`[SUPPLY_V2_ERROR] ${context}:`, error)
|
||||
// Алерты в систему мониторинга
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **1.1.3 Тестовая база данных**
|
||||
```bash
|
||||
# Создание отдельной БД для тестирования миграций
|
||||
npm run db:create:test-migration
|
||||
npm run db:migrate:test
|
||||
```
|
||||
|
||||
### **⚠️ ТОЧКА ПРОВЕРКИ 1.1:**
|
||||
- [ ] Feature flags работают
|
||||
- [ ] Мониторинг настроен
|
||||
- [ ] Тестовая БД создана
|
||||
- [ ] **ОТКАТ:** Отключить feature flags
|
||||
|
||||
**🔒 ROLLBACK PLAN 1.1:**
|
||||
```bash
|
||||
# Отключение feature flags
|
||||
export FULFILLMENT_CONSUMABLE_V2=false
|
||||
# Остановка мониторинга новых метрик
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ STEP 1.2: СОЗДАНИЕ СХЕМЫ БАЗЫ ДАННЫХ
|
||||
|
||||
### **Задачи:**
|
||||
1. ✅ Создание новых Prisma моделей
|
||||
2. ✅ Генерация и проверка миграций
|
||||
3. ✅ Применение миграций в test окружении
|
||||
|
||||
### **1.2.1 Prisma Schema Update**
|
||||
```prisma
|
||||
// Добавление в /prisma/schema.prisma
|
||||
model FulfillmentConsumableSupplyOrder {
|
||||
// ... полная схема из DATABASE_SCHEMA_V2.md
|
||||
}
|
||||
|
||||
model FulfillmentConsumableSupplyItem {
|
||||
// ... полная схема из DATABASE_SCHEMA_V2.md
|
||||
}
|
||||
|
||||
enum SupplyOrderStatusV2 {
|
||||
PENDING
|
||||
SUPPLIER_APPROVED
|
||||
LOGISTICS_CONFIRMED
|
||||
SHIPPED
|
||||
IN_TRANSIT
|
||||
DELIVERED
|
||||
CANCELLED
|
||||
}
|
||||
```
|
||||
|
||||
### **1.2.2 Создание миграции**
|
||||
```bash
|
||||
# Генерация миграции
|
||||
npx prisma migrate dev --name "add_fulfillment_consumable_supply_v2"
|
||||
|
||||
# Проверка SQL миграции ПЕРЕД применением
|
||||
cat prisma/migrations/xxx_add_fulfillment_consumable_supply_v2/migration.sql
|
||||
```
|
||||
|
||||
### **1.2.3 Тестирование миграции**
|
||||
```bash
|
||||
# Применение в тестовой БД
|
||||
npx prisma migrate deploy --schema=./prisma/schema.test.prisma
|
||||
|
||||
# Проверка создания таблиц
|
||||
npx prisma studio --schema=./prisma/schema.test.prisma
|
||||
```
|
||||
|
||||
### **⚠️ ТОЧКА ПРОВЕРКИ 1.2:**
|
||||
- [ ] Миграция создана успешно
|
||||
- [ ] SQL миграции проверен вручную
|
||||
- [ ] Таблицы созданы в тестовой БД
|
||||
- [ ] Все связи (FK) работают корректно
|
||||
- [ ] **ОТКАТ:** Revert migration
|
||||
|
||||
**🔒 ROLLBACK PLAN 1.2:**
|
||||
```bash
|
||||
# Откат миграции
|
||||
npx prisma migrate reset --force --skip-seed
|
||||
# ИЛИ ручной DROP TABLE если нужно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ STEP 1.3: GRAPHQL API
|
||||
|
||||
### **Задачи:**
|
||||
1. ✅ Создание GraphQL типов
|
||||
2. ✅ Создание resolvers с feature flag
|
||||
3. ✅ Тестирование API отдельно
|
||||
|
||||
### **1.3.1 GraphQL Types**
|
||||
```typescript
|
||||
// /src/graphql/typedefs.ts - ДОБАВИТЬ К СУЩЕСТВУЮЩИМ
|
||||
const fulfillmentConsumableTypes = `
|
||||
type FulfillmentConsumableSupplyOrder {
|
||||
id: ID!
|
||||
status: SupplyOrderStatusV2!
|
||||
fulfillmentCenterId: ID!
|
||||
supplierId: ID
|
||||
requestedDeliveryDate: DateTime!
|
||||
resalePricePerUnit: Float
|
||||
# ... все остальные поля
|
||||
items: [FulfillmentConsumableSupplyItem!]!
|
||||
}
|
||||
|
||||
type FulfillmentConsumableSupplyItem {
|
||||
id: ID!
|
||||
productId: ID!
|
||||
requestedQuantity: Int!
|
||||
unitPrice: Float!
|
||||
totalPrice: Float!
|
||||
product: Product!
|
||||
}
|
||||
|
||||
enum SupplyOrderStatusV2 {
|
||||
PENDING
|
||||
SUPPLIER_APPROVED
|
||||
LOGISTICS_CONFIRMED
|
||||
SHIPPED
|
||||
IN_TRANSIT
|
||||
DELIVERED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
input CreateFulfillmentConsumableSupplyInput {
|
||||
supplierId: ID!
|
||||
requestedDeliveryDate: DateTime!
|
||||
items: [FulfillmentConsumableSupplyItemInput!]!
|
||||
notes: String
|
||||
}
|
||||
|
||||
input FulfillmentConsumableSupplyItemInput {
|
||||
productId: ID!
|
||||
requestedQuantity: Int!
|
||||
}
|
||||
|
||||
type CreateFulfillmentConsumableSupplyResult {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
supplyOrder: FulfillmentConsumableSupplyOrder
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
# Новые запросы с feature flag
|
||||
myFulfillmentConsumableSupplies: [FulfillmentConsumableSupplyOrder!]!
|
||||
fulfillmentConsumableSupply(id: ID!): FulfillmentConsumableSupplyOrder
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
# Новые мутации с feature flag
|
||||
createFulfillmentConsumableSupply(
|
||||
input: CreateFulfillmentConsumableSupplyInput!
|
||||
): CreateFulfillmentConsumableSupplyResult!
|
||||
}
|
||||
`
|
||||
```
|
||||
|
||||
### **1.3.2 Resolvers с Feature Flags**
|
||||
```typescript
|
||||
// /src/graphql/resolvers.ts - ДОБАВИТЬ К СУЩЕСТВУЮЩИМ
|
||||
const fulfillmentConsumableResolvers = {
|
||||
Query: {
|
||||
myFulfillmentConsumableSupplies: async (_: unknown, __: unknown, context: Context) => {
|
||||
// 🚨 FEATURE FLAG ПРОВЕРКА
|
||||
if (!isFeatureEnabled('FULFILLMENT_CONSUMABLE_SUPPLY_V2')) {
|
||||
throw new GraphQLError('Feature not enabled')
|
||||
}
|
||||
|
||||
SupplySystemMonitor.logMigrationEvent('API_CALL', {
|
||||
operation: 'myFulfillmentConsumableSupplies',
|
||||
userId: context.user?.id
|
||||
})
|
||||
|
||||
try {
|
||||
// Полная реализация с системой безопасности
|
||||
// ... код реализации
|
||||
} catch (error) {
|
||||
SupplySystemMonitor.logError(error, 'myFulfillmentConsumableSupplies')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
fulfillmentConsumableSupply: async (_: unknown, args: { id: string }, context: Context) => {
|
||||
if (!isFeatureEnabled('FULFILLMENT_CONSUMABLE_SUPPLY_V2')) {
|
||||
throw new GraphQLError('Feature not enabled')
|
||||
}
|
||||
// ... реализация
|
||||
}
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
createFulfillmentConsumableSupply: async (_: unknown, args: any, context: Context) => {
|
||||
if (!isFeatureEnabled('FULFILLMENT_CONSUMABLE_SUPPLY_V2')) {
|
||||
return { success: false, message: 'Feature not enabled' }
|
||||
}
|
||||
|
||||
SupplySystemMonitor.logMigrationEvent('CREATE_SUPPLY', {
|
||||
supplierId: args.input.supplierId,
|
||||
itemsCount: args.input.items.length,
|
||||
userId: context.user?.id
|
||||
})
|
||||
|
||||
try {
|
||||
// Полная реализация создания поставки
|
||||
// ... код реализации
|
||||
|
||||
return { success: true, message: 'Supply created', supplyOrder: result }
|
||||
} catch (error) {
|
||||
SupplySystemMonitor.logError(error, 'createFulfillmentConsumableSupply')
|
||||
return { success: false, message: error.message }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **1.3.3 API Testing**
|
||||
```typescript
|
||||
// /tests/api/fulfillmentConsumableSupply.test.ts
|
||||
describe('FulfillmentConsumableSupply API', () => {
|
||||
beforeAll(() => {
|
||||
// Включаем feature flag для тестов
|
||||
process.env.FULFILLMENT_CONSUMABLE_V2 = 'true'
|
||||
})
|
||||
|
||||
test('should create supply order', async () => {
|
||||
const mutation = `
|
||||
mutation CreateFulfillmentConsumableSupply($input: CreateFulfillmentConsumableSupplyInput!) {
|
||||
createFulfillmentConsumableSupply(input: $input) {
|
||||
success
|
||||
message
|
||||
supplyOrder {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
// ... тестирование
|
||||
})
|
||||
|
||||
test('should not work when feature disabled', async () => {
|
||||
process.env.FULFILLMENT_CONSUMABLE_V2 = 'false'
|
||||
// Проверка что API недоступно
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### **⚠️ ТОЧКА ПРОВЕРКИ 1.3:**
|
||||
- [ ] GraphQL типы корректны
|
||||
- [ ] Resolvers работают с feature flag
|
||||
- [ ] API тесты проходят
|
||||
- [ ] Feature flag блокирует доступ когда выключен
|
||||
- [ ] **ОТКАТ:** Отключить feature flag
|
||||
|
||||
**🔒 ROLLBACK PLAN 1.3:**
|
||||
```bash
|
||||
# Отключение API через feature flag
|
||||
export FULFILLMENT_CONSUMABLE_V2=false
|
||||
# API станет недоступно мгновенно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 STEP 1.4: FRONTEND ИНТЕРФЕЙС
|
||||
|
||||
### **Задачи:**
|
||||
1. ✅ Создание формы создания поставки
|
||||
2. ✅ Создание страницы просмотра поставок
|
||||
3. ✅ Интеграция с существующим интерфейсом
|
||||
|
||||
### **1.4.1 Feature Flag в Frontend**
|
||||
```typescript
|
||||
// /src/lib/featureFlags.client.ts
|
||||
export function useFeatureFlag(flag: string) {
|
||||
return process.env.NEXT_PUBLIC_FULFILLMENT_CONSUMABLE_V2 === 'true'
|
||||
}
|
||||
```
|
||||
|
||||
### **1.4.2 Форма создания (с feature flag)**
|
||||
```typescript
|
||||
// /src/components/fulfillment-supplies/create-consumables-v2.tsx
|
||||
export function CreateConsumablesV2Page() {
|
||||
const featureEnabled = useFeatureFlag('FULFILLMENT_CONSUMABLE_V2')
|
||||
|
||||
if (!featureEnabled) {
|
||||
// Показываем старую версию или заглушку
|
||||
return <CreateConsumablesV1 />
|
||||
}
|
||||
|
||||
// Новая реализация
|
||||
return (
|
||||
<div>
|
||||
<h1>Создать поставку расходников ФФ (v2.0)</h1>
|
||||
{/* Полная форма */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### **1.4.3 Список поставок (с feature flag)**
|
||||
```typescript
|
||||
// /src/components/fulfillment-supplies/ff-consumables-v2.tsx
|
||||
export function FFConsumablesV2Tab() {
|
||||
const featureEnabled = useFeatureFlag('FULFILLMENT_CONSUMABLE_V2')
|
||||
|
||||
if (!featureEnabled) {
|
||||
return <FFConsumablesV1Tab />
|
||||
}
|
||||
|
||||
const { data, loading, error } = useQuery(GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES)
|
||||
|
||||
// Новая реализация отображения
|
||||
}
|
||||
```
|
||||
|
||||
### **1.4.4 Интеграция в роутинг**
|
||||
```typescript
|
||||
// /src/app/fulfillment-supplies/ff-consumables/page.tsx
|
||||
export default function FFConsumablesPage() {
|
||||
const featureEnabled = useFeatureFlag('FULFILLMENT_CONSUMABLE_V2')
|
||||
|
||||
return featureEnabled ? <FFConsumablesV2Tab /> : <FFConsumablesV1Tab />
|
||||
}
|
||||
```
|
||||
|
||||
### **⚠️ ТОЧКА ПРОВЕРКИ 1.4:**
|
||||
- [ ] Форма создания работает
|
||||
- [ ] Список поставок отображается
|
||||
- [ ] Feature flag переключает версии
|
||||
- [ ] Нет поломок в старом интерфейсе
|
||||
- [ ] **ОТКАТ:** Отключить frontend feature flag
|
||||
|
||||
**🔒 ROLLBACK PLAN 1.4:**
|
||||
```bash
|
||||
# Отключение новых компонентов
|
||||
export NEXT_PUBLIC_FULFILLMENT_CONSUMABLE_V2=false
|
||||
# Пользователи увидят старый интерфейс
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 STEP 1.5: ИНТЕГРАЦИЯ И END-TO-END ТЕСТИРОВАНИЕ
|
||||
|
||||
### **Задачи:**
|
||||
1. ✅ Полное тестирование workflow поставки
|
||||
2. ✅ Тестирование безопасности доступа
|
||||
3. ✅ Нагрузочное тестирование
|
||||
4. ✅ Проверка совместимости со старой системой
|
||||
|
||||
### **1.5.1 End-to-End тесты**
|
||||
```typescript
|
||||
// /tests/e2e/fulfillmentConsumableSupply.e2e.ts
|
||||
describe('Fulfillment Consumable Supply E2E', () => {
|
||||
test('Complete supply workflow', async () => {
|
||||
// 1. ФФ создает поставку
|
||||
// 2. Поставщик одобряет
|
||||
// 3. Логистика назначается
|
||||
// 4. Отгрузка
|
||||
// 5. Приемка
|
||||
// Проверяем каждый этап
|
||||
})
|
||||
|
||||
test('Security access control', async () => {
|
||||
// Проверяем что селлеры не видят поставки ФФ
|
||||
// Проверяем фильтрацию данных по ролям
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### **1.5.2 Параллельное тестирование**
|
||||
```typescript
|
||||
// /tests/parallel/oldVsNewSystem.test.ts
|
||||
describe('Old vs New System Compatibility', () => {
|
||||
test('Both systems work independently', async () => {
|
||||
// Создаем поставку в старой системе
|
||||
// Создаем поставку в новой системе
|
||||
// Проверяем что они не мешают друг другу
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### **1.5.3 Performance Testing**
|
||||
```bash
|
||||
# Нагрузочное тестирование API
|
||||
npm run test:load -- --scenario=fulfillment-consumable-v2
|
||||
```
|
||||
|
||||
### **⚠️ ТОЧКА ПРОВЕРКИ 1.5:**
|
||||
- [ ] E2E тесты проходят
|
||||
- [ ] Система безопасности работает
|
||||
- [ ] Производительность приемлемая
|
||||
- [ ] Старая система не затронута
|
||||
- [ ] **ОТКАТ:** Полное отключение feature flags
|
||||
|
||||
**🔒 ROLLBACK PLAN 1.5:**
|
||||
```bash
|
||||
# Полное отключение новой системы
|
||||
export FULFILLMENT_CONSUMABLE_V2=false
|
||||
export NEXT_PUBLIC_FULFILLMENT_CONSUMABLE_V2=false
|
||||
npm run restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 STEP 1.6: PRODUCTION DEPLOYMENT
|
||||
|
||||
### **Задачи:**
|
||||
1. ✅ Постепенное включение для пользователей
|
||||
2. ✅ Мониторинг в реальном времени
|
||||
3. ✅ Готовность к быстрому откату
|
||||
|
||||
### **1.6.1 Gradual Rollout**
|
||||
```typescript
|
||||
// Включение для 10% пользователей
|
||||
export function shouldUseV2ForUser(userId: string): boolean {
|
||||
if (!isFeatureEnabled('FULFILLMENT_CONSUMABLE_V2')) return false
|
||||
|
||||
// Хэш от userId, включаем для 10%
|
||||
const hash = hashCode(userId)
|
||||
return (hash % 10) === 0
|
||||
}
|
||||
```
|
||||
|
||||
### **1.6.2 Real-time мониторинг**
|
||||
```typescript
|
||||
// Метрики в реальном времени
|
||||
export const supplyV2Metrics = {
|
||||
createSuccess: 0,
|
||||
createError: 0,
|
||||
querySuccess: 0,
|
||||
queryError: 0,
|
||||
rollbackTriggered: false
|
||||
}
|
||||
|
||||
// Автоматический откат при критических ошибках
|
||||
if (supplyV2Metrics.createError > 10) {
|
||||
// Экстренное отключение
|
||||
process.env.FULFILLMENT_CONSUMABLE_V2 = 'false'
|
||||
supplyV2Metrics.rollbackTriggered = true
|
||||
}
|
||||
```
|
||||
|
||||
### **1.6.3 Production Checklist**
|
||||
```markdown
|
||||
## PRODUCTION DEPLOYMENT CHECKLIST
|
||||
- [ ] Database migration applied successfully
|
||||
- [ ] Feature flags configured
|
||||
- [ ] Monitoring dashboards active
|
||||
- [ ] Rollback procedures tested
|
||||
- [ ] Support team notified
|
||||
- [ ] Documentation updated
|
||||
- [ ] Gradual rollout configured (10% users)
|
||||
```
|
||||
|
||||
### **⚠️ ТОЧКА ПРОВЕРКИ 1.6:**
|
||||
- [ ] Deployment прошел успешно
|
||||
- [ ] 10% пользователей используют новую систему
|
||||
- [ ] Метрики показывают стабильность
|
||||
- [ ] Нет критических ошибок
|
||||
- [ ] **ОТКАТ:** Экстренное отключение
|
||||
|
||||
**🔒 ROLLBACK PLAN 1.6:**
|
||||
```bash
|
||||
# ЭКСТРЕННЫЙ ОТКАТ
|
||||
kubectl set env deployment/app FULFILLMENT_CONSUMABLE_V2=false
|
||||
# ИЛИ через admin panel
|
||||
curl -X POST /admin/feature-flags/disable/FULFILLMENT_CONSUMABLE_V2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 STEP 1.7: МОНИТОРИНГ И СТАБИЛИЗАЦИЯ
|
||||
|
||||
### **Задачи:**
|
||||
1. ✅ Анализ метрик производительности
|
||||
2. ✅ Сбор фидбека пользователей
|
||||
3. ✅ Исправление найденных проблем
|
||||
4. ✅ Подготовка к Phase 2
|
||||
|
||||
### **1.7.1 Мониторинг (1-2 недели)**
|
||||
```typescript
|
||||
// Ключевые метрики для отслеживания
|
||||
const metricsToWatch = {
|
||||
// Функциональные
|
||||
supplyCreationRate: 'число создаваемых поставок/день',
|
||||
successRate: 'процент успешных операций',
|
||||
averageProcessingTime: 'среднее время обработки',
|
||||
|
||||
// Технические
|
||||
databasePerformance: 'время ответа БД',
|
||||
apiLatency: 'задержка API',
|
||||
errorRate: 'процент ошибок',
|
||||
|
||||
// Бизнесовые
|
||||
userAdoption: 'процент пользователей использующих v2',
|
||||
userSatisfaction: 'оценка пользователей',
|
||||
supportTickets: 'количество тикетов поддержки'
|
||||
}
|
||||
```
|
||||
|
||||
### **1.7.2 Feedback Collection**
|
||||
```typescript
|
||||
// Встроенная система сбора фидбека
|
||||
export function FeedbackWidget() {
|
||||
return (
|
||||
<div className="feedback-widget">
|
||||
<p>Как вам новая система поставок?</p>
|
||||
<button onClick={() => sendFeedback('positive')}>👍</button>
|
||||
<button onClick={() => sendFeedback('negative')}>👎</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### **1.7.3 Анализ и улучшения**
|
||||
```markdown
|
||||
## КРИТЕРИИ УСПЕХА PHASE 1:
|
||||
✅ Успешность операций > 98%
|
||||
✅ Время отклика < 2 сек
|
||||
✅ Пользовательская оценка > 4/5
|
||||
✅ Количество багов < 5 критических
|
||||
✅ Готовность инфраструктуры к Phase 2
|
||||
```
|
||||
|
||||
### **⚠️ ТОЧКА ПРОВЕРКИ 1.7:**
|
||||
- [ ] Метрики в пределах нормы
|
||||
- [ ] Пользователи довольны
|
||||
- [ ] Критические баги исправлены
|
||||
- [ ] Система готова к расширению
|
||||
- [ ] **РЕШЕНИЕ:** Переход к Phase 2 или доработка
|
||||
|
||||
---
|
||||
|
||||
## 🎯 КРИТЕРИИ ГОТОВНОСТИ К PHASE 2
|
||||
|
||||
### ✅ **ОБЯЗАТЕЛЬНЫЕ УСЛОВИЯ:**
|
||||
1. **Функциональность:** Все features Phase 1 работают стабильно
|
||||
2. **Производительность:** Нет деградации по сравнению со старой системой
|
||||
3. **Безопасность:** Нет утечек данных, доступ контролируется корректно
|
||||
4. **Пользователи:** Положительные отзывы от 80%+ пользователей
|
||||
5. **Мониторинг:** Налаженная система отслеживания метрик
|
||||
|
||||
### 📈 **ЧИСЛЕННЫЕ ПОКАЗАТЕЛИ:**
|
||||
- Успешность создания поставок: **> 98%**
|
||||
- Время загрузки списка поставок: **< 2 сек**
|
||||
- Время создания поставки: **< 5 сек**
|
||||
- Uptime системы: **> 99.5%**
|
||||
- Количество критических багов: **= 0**
|
||||
|
||||
---
|
||||
|
||||
## 📋 PHASE 2: SELLER CONSUMABLE SUPPLY ORDERS
|
||||
|
||||
> **Сроки:** 2-3 недели после стабилизации Phase 1
|
||||
> **Риск:** СРЕДНИЙ (более сложная логика с хранением)
|
||||
|
||||
### **Ключевые особенности Phase 2:**
|
||||
1. **Мульти-организационность:** Селлер создает, ФФ хранит
|
||||
2. **Права доступа:** Сложная система доступа к данным
|
||||
3. **Сроки хранения:** Временные ограничения на хранение
|
||||
4. **Интеграция с Phase 1:** Должна работать вместе с поставками ФФ
|
||||
|
||||
### **Plan Phase 2:**
|
||||
- Step 2.1: Database Schema для селлерских расходников
|
||||
- Step 2.2: GraphQL API с правами доступа
|
||||
- Step 2.3: Frontend для селлеров и ФФ
|
||||
- Step 2.4: Интеграция с системой хранения
|
||||
- Step 2.5: Тестирование совместимости с Phase 1
|
||||
|
||||
---
|
||||
|
||||
## 📋 PHASE 3: GOODS SUPPLY ORDERS
|
||||
|
||||
> **Сроки:** 3-4 недели после стабилизации Phase 2
|
||||
> **Риск:** ВЫСОКИЙ (самая сложная система с рецептурами)
|
||||
|
||||
### **Ключевые особенности Phase 3:**
|
||||
1. **Рецептуры:** Сложная JSON структура с услугами
|
||||
2. **Мульти-компонентность:** Товары + услуги + расходники
|
||||
3. **Marketplace интеграция:** Связь с карточками товаров
|
||||
4. **Миграция данных:** Перенос существующих товарных поставок
|
||||
|
||||
---
|
||||
|
||||
## 📋 PHASE 4: ОЧИСТКА И ОПТИМИЗАЦИЯ
|
||||
|
||||
> **Сроки:** 1-2 недели
|
||||
> **Риск:** НИЗКИЙ (только после полного одобрения)
|
||||
|
||||
### **Задачи Phase 4:**
|
||||
1. **Миграция старых данных** (ТОЛЬКО с одобрения)
|
||||
2. **Удаление устаревшего кода** (ТОЛЬКО с одобрения)
|
||||
3. **Оптимизация производительности**
|
||||
4. **Финальное тестирование**
|
||||
|
||||
---
|
||||
|
||||
## 🚨 EMERGENCY PROCEDURES
|
||||
|
||||
### **🔴 CRITICAL ROLLBACK (при критических ошибках)**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# emergency-rollback.sh
|
||||
|
||||
echo "🚨 EMERGENCY ROLLBACK INITIATED"
|
||||
|
||||
# Отключение всех feature flags
|
||||
export FULFILLMENT_CONSUMABLE_V2=false
|
||||
export SELLER_CONSUMABLE_V2=false
|
||||
export GOODS_SUPPLY_V2=false
|
||||
export NEXT_PUBLIC_FULFILLMENT_CONSUMABLE_V2=false
|
||||
|
||||
# Перезапуск сервисов
|
||||
kubectl rollout restart deployment/app
|
||||
kubectl rollout restart deployment/api
|
||||
|
||||
# Уведомления
|
||||
curl -X POST "$SLACK_WEBHOOK" -d '{"text": "🚨 Supply System V2 Emergency Rollback Executed"}'
|
||||
|
||||
echo "✅ Rollback completed. System reverted to V1."
|
||||
```
|
||||
|
||||
### **🟡 PARTIAL ROLLBACK (при локальных проблемах)**
|
||||
```bash
|
||||
# Откат конкретной фазы
|
||||
rollback_phase_1() {
|
||||
export FULFILLMENT_CONSUMABLE_V2=false
|
||||
kubectl set env deployment/app FULFILLMENT_CONSUMABLE_V2=false
|
||||
}
|
||||
|
||||
rollback_phase_2() {
|
||||
export SELLER_CONSUMABLE_V2=false
|
||||
kubectl set env deployment/app SELLER_CONSUMABLE_V2=false
|
||||
}
|
||||
```
|
||||
|
||||
### **📊 MONITORING ALERTS**
|
||||
```typescript
|
||||
// Автоматические алерты
|
||||
const ALERT_THRESHOLDS = {
|
||||
ERROR_RATE: 5, // > 5% ошибок = алерт
|
||||
RESPONSE_TIME: 5000, // > 5 сек = алерт
|
||||
FAILED_CREATES: 10, // > 10 неудачных создания = алерт
|
||||
DATABASE_ERRORS: 3 // > 3 ошибки БД = критический алерт
|
||||
}
|
||||
|
||||
// Автоматическое отключение при превышении порогов
|
||||
if (currentMetrics.errorRate > ALERT_THRESHOLDS.ERROR_RATE) {
|
||||
triggerEmergencyRollback('HIGH_ERROR_RATE')
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 SUCCESS METRICS
|
||||
|
||||
### **📊 KPI для каждой фазы:**
|
||||
|
||||
#### **Phase 1 Success Criteria:**
|
||||
- ✅ 100% функций поставок расходников ФФ работают
|
||||
- ✅ 0 критических багов в production
|
||||
- ✅ < 2 сек время отклика API
|
||||
- ✅ > 90% положительных отзывов пользователей
|
||||
- ✅ 0 инцидентов безопасности
|
||||
|
||||
#### **Phase 2 Success Criteria:**
|
||||
- ✅ Поставки расходников селлеров работают корректно
|
||||
- ✅ Права доступа работают (селлер видит свое, ФФ видит ограниченно)
|
||||
- ✅ Интеграция с Phase 1 без конфликтов
|
||||
|
||||
#### **Phase 3 Success Criteria:**
|
||||
- ✅ Товарные поставки с рецептурами работают
|
||||
- ✅ Миграция существующих данных (если одобрена)
|
||||
- ✅ Интеграция с маркетплейсами
|
||||
|
||||
#### **Overall Success Criteria:**
|
||||
- ✅ **Производительность:** Не хуже старой системы
|
||||
- ✅ **Стабильность:** 99.9% uptime
|
||||
- ✅ **Безопасность:** 0 утечек данных
|
||||
- ✅ **Пользователи:** > 85% satisfaction rate
|
||||
- ✅ **Бизнес:** Увеличение эффективности процессов поставок
|
||||
|
||||
---
|
||||
|
||||
## ⏰ TIMELINE
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Supply System V2 Implementation Timeline
|
||||
dateFormat YYYY-MM-DD
|
||||
section Phase 1
|
||||
Infrastructure Setup :2024-08-25, 3d
|
||||
Database Schema :2024-08-28, 2d
|
||||
GraphQL API :2024-08-30, 4d
|
||||
Frontend Interface :2024-09-03, 4d
|
||||
Integration Testing :2024-09-07, 3d
|
||||
Production Deployment :2024-09-10, 2d
|
||||
Monitoring & Stabilization :2024-09-12, 7d
|
||||
|
||||
section Phase 2
|
||||
Seller Consumables Schema :2024-09-19, 3d
|
||||
Complex Access Control :2024-09-22, 5d
|
||||
Multi-org Frontend :2024-09-27, 4d
|
||||
Integration Testing :2024-10-01, 4d
|
||||
|
||||
section Phase 3
|
||||
Goods Supply Schema :2024-10-05, 4d
|
||||
Recipe System :2024-10-09, 6d
|
||||
Data Migration Prep :2024-10-15, 3d
|
||||
Testing & Validation :2024-10-18, 5d
|
||||
|
||||
section Phase 4
|
||||
Data Migration :2024-10-23, 3d
|
||||
Code Cleanup :2024-10-26, 2d
|
||||
Final Testing :2024-10-28, 2d
|
||||
```
|
||||
|
||||
### **🎯 MILESTONE DATES:**
|
||||
- **Phase 1 Complete:** September 19, 2024
|
||||
- **Phase 2 Complete:** October 4, 2024
|
||||
- **Phase 3 Complete:** October 23, 2024
|
||||
- **Full Migration Complete:** October 30, 2024
|
||||
|
||||
---
|
||||
|
||||
## ✅ ЗАКЛЮЧЕНИЕ
|
||||
|
||||
Данный план обеспечивает:
|
||||
|
||||
### 🛡️ **МАКСИМАЛЬНУЮ БЕЗОПАСНОСТЬ:**
|
||||
- Поэтапное внедрение без риска для работающей системы
|
||||
- Множественные точки отката на каждом этапе
|
||||
- Тщательное тестирование перед каждым шагом
|
||||
|
||||
### 📈 **КОНТРОЛИРУЕМЫЙ РОСТ:**
|
||||
- Постепенное увеличение сложности (ФФ → Селлер → Товары)
|
||||
- Feature flags для контроля доступа
|
||||
- Мониторинг метрик на каждом этапе
|
||||
|
||||
### 🔧 **ГИБКОСТЬ РЕАЛИЗАЦИИ:**
|
||||
- Возможность приостановить на любом этапе
|
||||
- Адаптация планов по результатам предыдущих фаз
|
||||
- Учет пожеланий пользователей
|
||||
|
||||
**Система готова к безопасной поэтапной реализации!**
|
@ -23,6 +23,9 @@ model User {
|
||||
sentMessages Message[] @relation("SentMessages")
|
||||
smsCodes SmsCode[]
|
||||
organization Organization? @relation(fields: [organizationId], references: [id])
|
||||
|
||||
// === НОВЫЕ СВЯЗИ С ПРИЕМКОЙ ПОСТАВОК V2 ===
|
||||
fulfillmentSupplyOrdersReceived FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersReceiver")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@ -120,6 +123,12 @@ model Organization {
|
||||
users User[]
|
||||
wbWarehouseCaches WBWarehouseCache[] @relation("WBWarehouseCaches")
|
||||
wildberriesSupplies WildberriesSupply[]
|
||||
|
||||
// === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 ===
|
||||
// Поставки расходников ФФ
|
||||
fulfillmentSupplyOrdersAsFulfillment FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersFulfillment")
|
||||
fulfillmentSupplyOrdersAsSupplier FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersSupplier")
|
||||
fulfillmentSupplyOrdersAsLogistics FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersLogistics")
|
||||
|
||||
@@index([referralCode])
|
||||
@@index([referredById])
|
||||
@ -283,6 +292,9 @@ model Product {
|
||||
category Category? @relation(fields: [categoryId], references: [id])
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
supplyOrderItems SupplyOrderItem[]
|
||||
|
||||
// === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 ===
|
||||
fulfillmentSupplyItems FulfillmentConsumableSupplyItem[] @relation("FFSupplyItems")
|
||||
|
||||
@@unique([organizationId, article])
|
||||
@@map("products")
|
||||
@ -732,3 +744,96 @@ enum SecurityAlertSeverity {
|
||||
HIGH
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// НОВАЯ СИСТЕМА ПОСТАВОК V2.0
|
||||
// ===============================================
|
||||
|
||||
// Новый enum для статусов поставок v2
|
||||
enum SupplyOrderStatusV2 {
|
||||
PENDING // Ожидает одобрения поставщика
|
||||
SUPPLIER_APPROVED // Одобрено поставщиком
|
||||
LOGISTICS_CONFIRMED // Логистика подтверждена
|
||||
SHIPPED // Отгружено поставщиком
|
||||
IN_TRANSIT // В пути
|
||||
DELIVERED // Доставлено и принято
|
||||
CANCELLED // Отменено
|
||||
}
|
||||
|
||||
// Модель для поставок расходников фулфилмента
|
||||
model FulfillmentConsumableSupplyOrder {
|
||||
// === БАЗОВЫЕ ПОЛЯ ===
|
||||
id String @id @default(cuid())
|
||||
status SupplyOrderStatusV2 @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === ДАННЫЕ ФФ (создатель) ===
|
||||
fulfillmentCenterId String // кто заказывает (FK: Organization)
|
||||
requestedDeliveryDate DateTime // когда нужно
|
||||
resalePricePerUnit Decimal? @db.Decimal(10, 2) // цена продажи селлерам
|
||||
minStockLevel Int? // минимальный остаток
|
||||
notes String? // заметки ФФ
|
||||
|
||||
// === ДАННЫЕ ПОСТАВЩИКА ===
|
||||
supplierId String? // кто поставляет (FK: Organization)
|
||||
supplierApprovedAt DateTime? // когда одобрил
|
||||
packagesCount Int? // количество грузомест
|
||||
estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³
|
||||
supplierContractId String? // номер договора
|
||||
supplierNotes String? // заметки поставщика
|
||||
|
||||
// === ДАННЫЕ ЛОГИСТИКИ ===
|
||||
logisticsPartnerId String? // кто везет (FK: Organization)
|
||||
estimatedDeliveryDate DateTime? // план доставки
|
||||
routeId String? // маршрут (FK: LogisticsRoute)
|
||||
logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки
|
||||
logisticsNotes String? // заметки логистики
|
||||
|
||||
// === ДАННЫЕ ОТГРУЗКИ ===
|
||||
shippedAt DateTime? // факт отгрузки
|
||||
trackingNumber String? // номер отслеживания
|
||||
|
||||
// === ДАННЫЕ ПРИЕМКИ ===
|
||||
receivedAt DateTime? // факт приемки
|
||||
receivedById String? // кто принял (FK: User)
|
||||
actualQuantity Int? // принято количество
|
||||
defectQuantity Int? // брак
|
||||
receiptNotes String? // заметки приемки
|
||||
|
||||
// === СВЯЗИ ===
|
||||
fulfillmentCenter Organization @relation("FFSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id])
|
||||
supplier Organization? @relation("FFSupplyOrdersSupplier", fields: [supplierId], references: [id])
|
||||
logisticsPartner Organization? @relation("FFSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id])
|
||||
receivedBy User? @relation("FFSupplyOrdersReceiver", fields: [receivedById], references: [id])
|
||||
items FulfillmentConsumableSupplyItem[]
|
||||
|
||||
@@map("fulfillment_consumable_supply_orders")
|
||||
}
|
||||
|
||||
model FulfillmentConsumableSupplyItem {
|
||||
id String @id @default(cuid())
|
||||
supplyOrderId String // связь с поставкой
|
||||
productId String // какой расходник (FK: Product)
|
||||
|
||||
// === КОЛИЧЕСТВА ===
|
||||
requestedQuantity Int // запросили
|
||||
approvedQuantity Int? // поставщик одобрил
|
||||
shippedQuantity Int? // отгрузили
|
||||
receivedQuantity Int? // приняли
|
||||
defectQuantity Int? @default(0) // брак
|
||||
|
||||
// === ЦЕНЫ ===
|
||||
unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика
|
||||
totalPrice Decimal @db.Decimal(12, 2) // общая стоимость
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// === СВЯЗИ ===
|
||||
supplyOrder FulfillmentConsumableSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation("FFSupplyItems", fields: [productId], references: [id])
|
||||
|
||||
@@unique([supplyOrderId, productId])
|
||||
@@map("fulfillment_consumable_supply_items")
|
||||
}
|
||||
|
5
src/app/create-fulfillment-consumables-v2/page.tsx
Normal file
5
src/app/create-fulfillment-consumables-v2/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import CreateFulfillmentConsumablesSupplyV2Page from '@/components/fulfillment-supplies/create-fulfillment-consumables-supply-v2'
|
||||
|
||||
export default function Page() {
|
||||
return <CreateFulfillmentConsumablesSupplyV2Page />
|
||||
}
|
9
src/app/fulfillment-supplies/consumables/page.tsx
Normal file
9
src/app/fulfillment-supplies/consumables/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { FulfillmentConsumablesOrdersTab } from '@/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab'
|
||||
|
||||
export default function ConsumablesPage() {
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<FulfillmentConsumablesOrdersTab />
|
||||
</div>
|
||||
)
|
||||
}
|
9
src/app/fulfillment-supplies/detailed-supplies/page.tsx
Normal file
9
src/app/fulfillment-supplies/detailed-supplies/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { FulfillmentDetailedSuppliesTab } from '@/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab'
|
||||
|
||||
export default function DetailedSuppliesPage() {
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<FulfillmentDetailedSuppliesTab />
|
||||
</div>
|
||||
)
|
||||
}
|
10
src/app/fulfillment-supplies/goods/new/page.tsx
Normal file
10
src/app/fulfillment-supplies/goods/new/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { FulfillmentGoodsOrdersTab } from '@/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-orders-tab'
|
||||
|
||||
export default function GoodsNewPage() {
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<h3 className="text-white font-semibold mb-4">Новые товары</h3>
|
||||
<FulfillmentGoodsOrdersTab />
|
||||
</div>
|
||||
)
|
||||
}
|
8
src/app/fulfillment-supplies/goods/received/page.tsx
Normal file
8
src/app/fulfillment-supplies/goods/received/page.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export default function GoodsReceivedPage() {
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<h3 className="text-white font-semibold mb-4">Принятые товары</h3>
|
||||
<div className="text-white/80">Здесь отображаются принятые на склад товары</div>
|
||||
</div>
|
||||
)
|
||||
}
|
8
src/app/fulfillment-supplies/goods/receiving/page.tsx
Normal file
8
src/app/fulfillment-supplies/goods/receiving/page.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export default function GoodsReceivingPage() {
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<h3 className="text-white font-semibold mb-4">Товары в приёмке</h3>
|
||||
<div className="text-white/80">Здесь отображаются товары в процессе приёмки на склад фулфилмента</div>
|
||||
</div>
|
||||
)
|
||||
}
|
16
src/app/fulfillment-supplies/layout.tsx
Normal file
16
src/app/fulfillment-supplies/layout.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { AuthGuard } from '@/components/auth-guard'
|
||||
import { FulfillmentSuppliesLayout } from '@/components/fulfillment-supplies/fulfillment-supplies-layout'
|
||||
|
||||
export default function FulfillmentSuppliesLayoutPage({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<FulfillmentSuppliesLayout>
|
||||
{children}
|
||||
</FulfillmentSuppliesLayout>
|
||||
</AuthGuard>
|
||||
)
|
||||
}
|
@ -1,10 +1,6 @@
|
||||
import { AuthGuard } from '@/components/auth-guard'
|
||||
import { FulfillmentSuppliesDashboard } from '@/components/fulfillment-supplies/fulfillment-supplies-dashboard'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function FulfillmentSuppliesPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<FulfillmentSuppliesDashboard />
|
||||
</AuthGuard>
|
||||
)
|
||||
// Редирект на дефолтный таб - товары новые
|
||||
redirect('/fulfillment-supplies/goods/new')
|
||||
}
|
||||
|
9
src/app/fulfillment-supplies/returns/page.tsx
Normal file
9
src/app/fulfillment-supplies/returns/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { PvzReturnsTab } from '@/components/fulfillment-supplies/fulfillment-supplies/pvz-returns-tab'
|
||||
|
||||
export default function ReturnsPage() {
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<PvzReturnsTab />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import CreateFulfillmentConsumablesSupplyV2Page from '@/components/fulfillment-supplies/create-fulfillment-consumables-supply-v2'
|
||||
|
||||
export default function Page() {
|
||||
return <CreateFulfillmentConsumablesSupplyV2Page />
|
||||
}
|
@ -218,7 +218,7 @@ export function Sidebar({ isRootInstance = false }: { isRootInstance?: boolean }
|
||||
// Для каждого типа кабинета свой роут
|
||||
switch (user?.organization?.type) {
|
||||
case 'FULFILLMENT':
|
||||
router.push('/fulfillment-supplies')
|
||||
router.push('/fulfillment-supplies/goods/new')
|
||||
break
|
||||
case 'SELLER':
|
||||
router.push('/supplies')
|
||||
|
@ -52,6 +52,8 @@ interface FulfillmentConsumableProduct {
|
||||
}
|
||||
stock?: number
|
||||
unit?: string
|
||||
quantity?: number
|
||||
ordered?: number
|
||||
}
|
||||
|
||||
interface SelectedFulfillmentConsumable {
|
||||
|
@ -0,0 +1,821 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { ArrowLeft, Building2, Search, Package, Plus, Minus, ShoppingCart, Wrench } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { OrganizationAvatar } from '@/components/market/organization-avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { CREATE_FULFILLMENT_CONSUMABLE_SUPPLY, GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES } from '@/graphql/queries/fulfillment-consumables-v2'
|
||||
import {
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_ORGANIZATION_PRODUCTS,
|
||||
} from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
interface FulfillmentConsumableSupplier {
|
||||
id: string
|
||||
inn: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
|
||||
address?: string
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
users?: Array<{ id: string; avatar?: string; managerName?: string }>
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface FulfillmentConsumableProduct {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
price: number
|
||||
type?: 'PRODUCT' | 'CONSUMABLE'
|
||||
category?: { name: string }
|
||||
images: string[]
|
||||
mainImage?: string
|
||||
organization: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
stock?: number
|
||||
unit?: string
|
||||
quantity?: number
|
||||
ordered?: number
|
||||
}
|
||||
|
||||
interface SelectedFulfillmentConsumable {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
selectedQuantity: number
|
||||
unit?: string
|
||||
category?: string
|
||||
supplierId: string
|
||||
supplierName: string
|
||||
}
|
||||
|
||||
export default function CreateFulfillmentConsumablesSupplyV2Page() {
|
||||
const router = useRouter()
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
const { user } = useAuth()
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<FulfillmentConsumableSupplier | null>(null)
|
||||
const [selectedLogistics, setSelectedLogistics] = useState<FulfillmentConsumableSupplier | null>(null)
|
||||
const [selectedConsumables, setSelectedConsumables] = useState<SelectedFulfillmentConsumable[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [productSearchQuery, setProductSearchQuery] = useState('')
|
||||
const [deliveryDate, setDeliveryDate] = useState('')
|
||||
const [notes, setNotes] = useState('')
|
||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false)
|
||||
|
||||
// Загружаем контрагентов-поставщиков расходников
|
||||
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
|
||||
|
||||
// Убираем избыточное логирование для предотвращения визуального "бесконечного цикла"
|
||||
|
||||
// Стабилизируем переменные для useQuery
|
||||
const queryVariables = useMemo(() => {
|
||||
return {
|
||||
organizationId: selectedSupplier?.id || '', // Всегда возвращаем объект, но с пустым ID если нет поставщика
|
||||
search: productSearchQuery || null,
|
||||
category: null,
|
||||
type: 'CONSUMABLE' as const, // Фильтруем только расходники согласно rules2.md
|
||||
}
|
||||
}, [selectedSupplier?.id, productSearchQuery])
|
||||
|
||||
// Загружаем товары для выбранного поставщика с фильтрацией по типу CONSUMABLE
|
||||
const {
|
||||
data: productsData,
|
||||
loading: productsLoading,
|
||||
error: _productsError,
|
||||
} = useQuery(GET_ORGANIZATION_PRODUCTS, {
|
||||
skip: !selectedSupplier?.id, // Используем стабильное условие вместо !queryVariables
|
||||
variables: queryVariables,
|
||||
onCompleted: (data) => {
|
||||
// Логируем только количество загруженных товаров
|
||||
console.warn(`📦 Загружено товаров: ${data?.organizationProducts?.length || 0}`)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('❌ GET_ORGANIZATION_PRODUCTS ERROR:', error)
|
||||
},
|
||||
})
|
||||
|
||||
// Мутация для создания заказа поставки расходников v2
|
||||
const [createSupply] = useMutation(CREATE_FULFILLMENT_CONSUMABLE_SUPPLY)
|
||||
|
||||
// Фильтруем только поставщиков расходников (поставщиков)
|
||||
const consumableSuppliers = (counterpartiesData?.myCounterparties || []).filter(
|
||||
(org: FulfillmentConsumableSupplier) => org.type === 'WHOLESALE',
|
||||
)
|
||||
|
||||
// Фильтруем только логистические компании
|
||||
const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter(
|
||||
(org: FulfillmentConsumableSupplier) => org.type === 'LOGIST',
|
||||
)
|
||||
|
||||
// Фильтруем поставщиков по поисковому запросу
|
||||
const filteredSuppliers = consumableSuppliers.filter(
|
||||
(supplier: FulfillmentConsumableSupplier) =>
|
||||
supplier.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
supplier.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
supplier.inn?.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
// Фильтруем товары по выбранному поставщику
|
||||
// 📦 Получаем товары поставщика (уже отфильтрованы в GraphQL запросе по типу CONSUMABLE)
|
||||
const supplierProducts = productsData?.organizationProducts || []
|
||||
|
||||
// Отладочное логирование только при смене поставщика
|
||||
React.useEffect(() => {
|
||||
if (selectedSupplier) {
|
||||
console.warn('🔄 ПОСТАВЩИК ВЫБРАН:', {
|
||||
id: selectedSupplier.id,
|
||||
name: selectedSupplier.name || selectedSupplier.fullName,
|
||||
type: selectedSupplier.type,
|
||||
})
|
||||
}
|
||||
}, [selectedSupplier]) // Включаем весь объект поставщика для корректной работы
|
||||
|
||||
// Логируем результат загрузки товаров только при получении данных
|
||||
React.useEffect(() => {
|
||||
if (productsData && !productsLoading) {
|
||||
console.warn('📦 ТОВАРЫ ЗАГРУЖЕНЫ:', {
|
||||
organizationProductsCount: productsData?.organizationProducts?.length || 0,
|
||||
supplierProductsCount: supplierProducts.length,
|
||||
})
|
||||
}
|
||||
}, [productsData, productsLoading, supplierProducts.length]) // Включаем все зависимости для корректной работы
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: 'RUB',
|
||||
minimumFractionDigits: 0,
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const updateConsumableQuantity = (productId: string, quantity: number) => {
|
||||
const product = supplierProducts.find((p: FulfillmentConsumableProduct) => p.id === productId)
|
||||
if (!product || !selectedSupplier) return
|
||||
|
||||
// 🔒 ВАЛИДАЦИЯ ОСТАТКОВ согласно правилам (раздел 6.2)
|
||||
if (quantity > 0) {
|
||||
const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0)
|
||||
|
||||
if (quantity > availableStock) {
|
||||
toast.error(`❌ Недостаточно остатков!\nДоступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedConsumables((prev) => {
|
||||
const existing = prev.find((p) => p.id === productId)
|
||||
|
||||
if (quantity === 0) {
|
||||
// Удаляем расходник если количество 0
|
||||
return prev.filter((p) => p.id !== productId)
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
// Обновляем количество существующего расходника
|
||||
return prev.map((p) => (p.id === productId ? { ...p, selectedQuantity: quantity } : p))
|
||||
} else {
|
||||
// Добавляем новый расходник
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
selectedQuantity: quantity,
|
||||
unit: product.unit || 'шт',
|
||||
category: product.category?.name || 'Расходники',
|
||||
supplierId: selectedSupplier.id,
|
||||
supplierName: selectedSupplier.name || selectedSupplier.fullName || 'Поставщик',
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSelectedQuantity = (productId: string): number => {
|
||||
const selected = selectedConsumables.find((p) => p.id === productId)
|
||||
return selected ? selected.selectedQuantity : 0
|
||||
}
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return selectedConsumables.reduce((sum, consumable) => sum + consumable.price * consumable.selectedQuantity, 0)
|
||||
}
|
||||
|
||||
const getTotalItems = () => {
|
||||
return selectedConsumables.reduce((sum, consumable) => sum + consumable.selectedQuantity, 0)
|
||||
}
|
||||
|
||||
const handleCreateSupply = async () => {
|
||||
if (!selectedSupplier || selectedConsumables.length === 0 || !deliveryDate) {
|
||||
toast.error('Заполните все обязательные поля: поставщик, расходники и дата доставки')
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreatingSupply(true)
|
||||
|
||||
try {
|
||||
// Новый формат для системы v2
|
||||
const input = {
|
||||
supplierId: selectedSupplier.id,
|
||||
requestedDeliveryDate: deliveryDate,
|
||||
items: selectedConsumables.map((consumable) => ({
|
||||
productId: consumable.id,
|
||||
requestedQuantity: consumable.selectedQuantity,
|
||||
})),
|
||||
notes: notes || undefined,
|
||||
}
|
||||
|
||||
console.warn('🚀 СОЗДАНИЕ ПОСТАВКИ v2 - INPUT:', input)
|
||||
|
||||
const result = await createSupply({
|
||||
variables: { input },
|
||||
refetchQueries: [
|
||||
{ query: GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES }, // Обновляем новый v2 запрос
|
||||
],
|
||||
})
|
||||
|
||||
console.warn('🎯 РЕЗУЛЬТАТ СОЗДАНИЯ ПОСТАВКИ v2:', result)
|
||||
|
||||
if (result.data?.createFulfillmentConsumableSupply?.success) {
|
||||
toast.success('Поставка расходников создана успешно!')
|
||||
// Очищаем форму
|
||||
setSelectedSupplier(null)
|
||||
setSelectedLogistics(null)
|
||||
setSelectedConsumables([])
|
||||
setDeliveryDate('')
|
||||
setProductSearchQuery('')
|
||||
setSearchQuery('')
|
||||
setNotes('')
|
||||
|
||||
// Перенаправляем на страницу детальных поставок
|
||||
router.push('/fulfillment-supplies/detailed-supplies')
|
||||
} else {
|
||||
toast.error(result.data?.createFulfillmentConsumableSupply?.message || 'Ошибка при создании поставки')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating fulfillment consumables supply v2:', error)
|
||||
toast.error('Ошибка при создании поставки расходников')
|
||||
} finally {
|
||||
setIsCreatingSupply(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}>
|
||||
<div className="min-h-full w-full flex flex-col px-3 py-2">
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center justify-between mb-3 flex-shrink-0">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white mb-1">Создание поставки расходников фулфилмента</h1>
|
||||
<p className="text-white/60 text-sm">
|
||||
Выберите поставщика и добавьте расходники в заказ для вашего фулфилмент-центра
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push('/fulfillment-supplies/detailed-supplies')}
|
||||
className="text-white/60 hover:text-white hover:bg-white/10 text-sm"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Назад
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Основной контент с двумя блоками */}
|
||||
<div className="flex-1 flex gap-3 min-h-0">
|
||||
{/* Левая колонка - Поставщики и Расходники */}
|
||||
<div className="flex-1 flex flex-col gap-3 min-h-0">
|
||||
{/* Блок "Поставщики" */}
|
||||
<Card className="bg-gradient-to-r from-white/15 via-white/10 to-white/15 backdrop-blur-xl border border-white/30 shadow-2xl flex-shrink-0 sticky top-0 z-10 rounded-xl overflow-hidden">
|
||||
<div className="p-3 bg-gradient-to-r from-purple-500/10 to-pink-500/10">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h2 className="text-lg font-bold flex items-center flex-shrink-0 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
<Building2 className="h-5 w-5 mr-3 text-purple-400" />
|
||||
Поставщики расходников
|
||||
</h2>
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-purple-300 h-4 w-4 z-10" />
|
||||
<Input
|
||||
placeholder="Найти поставщика..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-white/20 backdrop-blur border-white/30 text-white placeholder-white/50 pl-10 h-8 text-sm rounded-full shadow-inner focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50 transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
{selectedSupplier && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedSupplier(null)}
|
||||
className="text-white/70 hover:text-white hover:bg-white/20 text-sm h-8 px-3 flex-shrink-0 rounded-full transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
✕ Сбросить
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 pb-3 h-24 overflow-hidden">
|
||||
{counterpartiesLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-purple-400 border-t-transparent mx-auto mb-2"></div>
|
||||
<p className="text-white/70 text-sm font-medium">Загружаем поставщиков...</p>
|
||||
</div>
|
||||
) : filteredSuppliers.length === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-3 w-fit mx-auto mb-2">
|
||||
<Building2 className="h-6 w-6 text-purple-300" />
|
||||
</div>
|
||||
<p className="text-white/70 text-sm font-medium">
|
||||
{searchQuery ? 'Поставщики не найдены' : 'Добавьте поставщиков'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2 h-full pt-1">
|
||||
{filteredSuppliers.slice(0, 7).map((supplier: FulfillmentConsumableSupplier, index: number) => (
|
||||
<Card
|
||||
key={supplier.id}
|
||||
className={`relative cursor-pointer transition-all duration-300 border flex-shrink-0 rounded-xl overflow-hidden group hover:scale-105 hover:shadow-xl ${
|
||||
selectedSupplier?.id === supplier.id
|
||||
? 'bg-gradient-to-br from-orange-500/30 via-orange-400/20 to-orange-500/30 border-orange-400/60 shadow-lg shadow-orange-500/25'
|
||||
: 'bg-gradient-to-br from-white/10 via-white/5 to-white/10 border-white/20 hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40'
|
||||
}`}
|
||||
style={{
|
||||
width: 'calc((100% - 48px) / 7)', // 48px = 6 gaps * 8px each
|
||||
animationDelay: `${index * 100}ms`,
|
||||
}}
|
||||
onClick={() => setSelectedSupplier(supplier)}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center h-full p-2 space-y-1">
|
||||
<div className="relative">
|
||||
<OrganizationAvatar
|
||||
organization={{
|
||||
id: supplier.id,
|
||||
name: supplier.name || supplier.fullName || 'Поставщик',
|
||||
fullName: supplier.fullName,
|
||||
users: (supplier.users || []).map((user) => ({
|
||||
id: user.id,
|
||||
avatar: user.avatar,
|
||||
})),
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
{selectedSupplier?.id === supplier.id && (
|
||||
<div className="absolute -top-1 -right-1 bg-gradient-to-r from-orange-400 to-orange-500 rounded-full w-4 h-4 flex items-center justify-center shadow-lg animate-pulse">
|
||||
<span className="text-white text-xs font-bold">✓</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center w-full space-y-0.5">
|
||||
<h3 className="text-white font-semibold text-xs truncate leading-tight group-hover:text-purple-200 transition-colors duration-300">
|
||||
{(supplier.name || supplier.fullName || 'Поставщик').slice(0, 10)}
|
||||
</h3>
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
<span className="text-yellow-400 text-sm animate-pulse">★</span>
|
||||
<span className="text-white/80 text-xs font-medium">4.5</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/10 rounded-full h-1 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-purple-400 to-pink-400 h-full rounded-full animate-pulse"
|
||||
style={{ width: '90%' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover эффект */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/0 to-pink-500/0 group-hover:from-purple-500/10 group-hover:to-pink-500/10 transition-all duration-300 pointer-events-none"></div>
|
||||
</Card>
|
||||
))}
|
||||
{filteredSuppliers.length > 7 && (
|
||||
<div
|
||||
className="flex-shrink-0 flex flex-col items-center justify-center bg-gradient-to-br from-white/10 to-white/5 rounded-xl border border-white/20 text-white/70 hover:text-white transition-all duration-300 hover:scale-105"
|
||||
style={{ width: 'calc((100% - 48px) / 7)' }}
|
||||
>
|
||||
<div className="text-lg font-bold text-purple-300">+{filteredSuppliers.length - 7}</div>
|
||||
<div className="text-xs">ещё</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Блок "Расходники" */}
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 flex-1 min-h-0 flex flex-col">
|
||||
<div className="p-3 border-b border-white/10 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center">
|
||||
<Wrench className="h-4 w-4 mr-2" />
|
||||
Расходники для фулфилмента
|
||||
{selectedSupplier && (
|
||||
<span className="text-white/60 text-xs font-normal ml-2 truncate">
|
||||
- {selectedSupplier.name || selectedSupplier.fullName}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
{selectedSupplier && (
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 text-white/40 h-3 w-3" />
|
||||
<Input
|
||||
placeholder="Поиск расходников..."
|
||||
value={productSearchQuery}
|
||||
onChange={(e) => setProductSearchQuery(e.target.value)}
|
||||
className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-7 h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 flex-1 overflow-y-auto">
|
||||
{!selectedSupplier ? (
|
||||
<div className="text-center py-8">
|
||||
<Wrench className="h-8 w-8 text-white/40 mx-auto mb-3" />
|
||||
<p className="text-white/60 text-sm">Выберите поставщика для просмотра расходников</p>
|
||||
</div>
|
||||
) : productsLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent mx-auto mb-2"></div>
|
||||
<p className="text-white/60 text-sm">Загрузка...</p>
|
||||
</div>
|
||||
) : supplierProducts.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Package className="h-8 w-8 text-white/40 mx-auto mb-3" />
|
||||
<p className="text-white/60 text-sm">Нет доступных расходников</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 gap-3">
|
||||
{supplierProducts.map((product: FulfillmentConsumableProduct, index: number) => {
|
||||
const selectedQuantity = getSelectedQuantity(product.id)
|
||||
return (
|
||||
<Card
|
||||
key={product.id}
|
||||
className={`relative bg-gradient-to-br from-white/10 via-white/5 to-white/10 backdrop-blur border border-white/20 p-3 rounded-xl overflow-hidden group hover:shadow-xl transition-all duration-300 ${
|
||||
selectedQuantity > 0
|
||||
? 'ring-2 ring-green-400/50 bg-gradient-to-br from-green-500/20 via-green-400/10 to-green-500/20'
|
||||
: 'hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40'
|
||||
}`}
|
||||
style={{
|
||||
animationDelay: `${index * 50}ms`,
|
||||
minHeight: '200px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="space-y-2 h-full flex flex-col">
|
||||
{/* Изображение товара */}
|
||||
<div className="aspect-square bg-white/5 rounded-lg overflow-hidden relative flex-shrink-0">
|
||||
{/* 🚫 ОВЕРЛЕЙ НЕДОСТУПНОСТИ */}
|
||||
{(() => {
|
||||
const totalStock = product.stock || (product as any).quantity || 0
|
||||
const orderedStock = (product as any).ordered || 0
|
||||
const availableStock = totalStock - orderedStock
|
||||
|
||||
if (availableStock <= 0) {
|
||||
return (
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-10">
|
||||
<div className="text-center">
|
||||
<div className="text-red-400 font-bold text-xs">НЕТ В НАЛИЧИИ</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
{product.images && product.images.length > 0 && product.images[0] ? (
|
||||
<Image
|
||||
src={product.images[0]}
|
||||
alt={product.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
) : product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Wrench className="h-8 w-8 text-white/40" />
|
||||
</div>
|
||||
)}
|
||||
{selectedQuantity > 0 && (
|
||||
<div className="absolute top-2 right-2 bg-gradient-to-r from-green-400 to-green-500 rounded-full w-6 h-6 flex items-center justify-center shadow-lg animate-pulse">
|
||||
<span className="text-white text-xs font-bold">
|
||||
{selectedQuantity > 999 ? '999+' : selectedQuantity}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация о товаре */}
|
||||
<div className="space-y-1 flex-grow">
|
||||
<h3 className="text-white font-medium text-sm leading-tight line-clamp-2 group-hover:text-purple-200 transition-colors duration-300">
|
||||
{product.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{product.category && (
|
||||
<Badge className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs px-2 py-1">
|
||||
{product.category.name.slice(0, 10)}
|
||||
</Badge>
|
||||
)}
|
||||
{/* 🚨 ИНДИКАТОР НИЗКИХ ОСТАТКОВ согласно правилам (раздел 6.3) */}
|
||||
{(() => {
|
||||
const totalStock = product.stock || product.quantity || 0
|
||||
const orderedStock = product.ordered || 0
|
||||
const availableStock = totalStock - orderedStock
|
||||
|
||||
if (availableStock <= 0) {
|
||||
return (
|
||||
<Badge className="bg-red-500/30 text-red-300 border-red-500/50 text-xs px-2 py-1 animate-pulse">
|
||||
Нет в наличии
|
||||
</Badge>
|
||||
)
|
||||
} else if (availableStock <= 10) {
|
||||
return (
|
||||
<Badge className="bg-yellow-500/30 text-yellow-300 border-yellow-500/50 text-xs px-2 py-1">
|
||||
Мало остатков
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-green-400 font-semibold text-sm">
|
||||
{formatCurrency(product.price)}
|
||||
</span>
|
||||
{/* 📊 АКТУАЛЬНЫЙ ОСТАТОК согласно правилам (раздел 6.4.2) */}
|
||||
<div className="text-right">
|
||||
{(() => {
|
||||
const totalStock = product.stock || product.quantity || 0
|
||||
const orderedStock = product.ordered || 0
|
||||
const availableStock = totalStock - orderedStock
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end">
|
||||
<span
|
||||
className={`text-xs font-medium ${
|
||||
availableStock <= 0
|
||||
? 'text-red-400'
|
||||
: availableStock <= 10
|
||||
? 'text-yellow-400'
|
||||
: 'text-white/80'
|
||||
}`}
|
||||
>
|
||||
Доступно: {availableStock}
|
||||
</span>
|
||||
{orderedStock > 0 && (
|
||||
<span className="text-white/40 text-xs">Заказано: {orderedStock}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Управление количеством */}
|
||||
<div className="flex flex-col items-center space-y-2 mt-auto">
|
||||
{(() => {
|
||||
const totalStock = product.stock || (product as any).quantity || 0
|
||||
const orderedStock = (product as any).ordered || 0
|
||||
const availableStock = totalStock - orderedStock
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
updateConsumableQuantity(product.id, Math.max(0, selectedQuantity - 1))
|
||||
}
|
||||
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/20 rounded-full transition-all duration-300"
|
||||
disabled={selectedQuantity === 0}
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</Button>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={selectedQuantity === 0 ? '' : selectedQuantity.toString()}
|
||||
onChange={(e) => {
|
||||
let inputValue = e.target.value
|
||||
|
||||
// Удаляем все нецифровые символы
|
||||
inputValue = inputValue.replace(/[^0-9]/g, '')
|
||||
|
||||
// Удаляем ведущие нули
|
||||
inputValue = inputValue.replace(/^0+/, '')
|
||||
|
||||
// Если строка пустая после удаления нулей, устанавливаем 0
|
||||
const numericValue = inputValue === '' ? 0 : parseInt(inputValue)
|
||||
|
||||
// Ограничиваем значение максимумом доступного остатка
|
||||
const clampedValue = Math.min(numericValue, availableStock, 99999)
|
||||
|
||||
updateConsumableQuantity(product.id, clampedValue)
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// При потере фокуса, если поле пустое, устанавливаем 0
|
||||
if (e.target.value === '') {
|
||||
updateConsumableQuantity(product.id, 0)
|
||||
}
|
||||
}}
|
||||
className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
|
||||
placeholder="0"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
Math.min(selectedQuantity + 1, availableStock, 99999),
|
||||
)
|
||||
}
|
||||
className={`h-6 w-6 p-0 rounded-full transition-all duration-300 ${
|
||||
selectedQuantity >= availableStock || availableStock <= 0
|
||||
? 'text-white/30 cursor-not-allowed'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/20'
|
||||
}`}
|
||||
disabled={selectedQuantity >= availableStock || availableStock <= 0}
|
||||
title={
|
||||
availableStock <= 0
|
||||
? 'Товар отсутствует на складе'
|
||||
: selectedQuantity >= availableStock
|
||||
? `Максимум доступно: ${availableStock}`
|
||||
: 'Увеличить количество'
|
||||
}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{selectedQuantity > 0 && (
|
||||
<div className="text-center">
|
||||
<span className="text-green-400 font-bold text-sm bg-green-500/10 px-3 py-1 rounded-full">
|
||||
{formatCurrency(product.price * selectedQuantity)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover эффект */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/0 to-pink-500/0 group-hover:from-purple-500/5 group-hover:to-pink-500/5 transition-all duration-300 pointer-events-none rounded-xl"></div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Правая колонка - Корзина */}
|
||||
<div className="w-72 flex-shrink-0">
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-3 sticky top-0">
|
||||
<h3 className="text-white font-semibold mb-3 flex items-center text-sm">
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Корзина ({getTotalItems()} шт)
|
||||
</h3>
|
||||
|
||||
{selectedConsumables.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-4 w-fit mx-auto mb-3">
|
||||
<ShoppingCart className="h-8 w-8 text-purple-300" />
|
||||
</div>
|
||||
<p className="text-white/60 text-sm font-medium mb-2">Корзина пуста</p>
|
||||
<p className="text-white/40 text-xs mb-3">Добавьте расходники для создания поставки</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 mb-3 max-h-48 overflow-y-auto">
|
||||
{selectedConsumables.map((consumable) => (
|
||||
<div key={consumable.id} className="flex items-center justify-between p-2 bg-white/5 rounded-lg">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white text-xs font-medium truncate">{consumable.name}</p>
|
||||
<p className="text-white/60 text-xs">
|
||||
{formatCurrency(consumable.price)} × {consumable.selectedQuantity}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-green-400 font-medium text-xs">
|
||||
{formatCurrency(consumable.price * consumable.selectedQuantity)}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateConsumableQuantity(consumable.id, 0)}
|
||||
className="h-5 w-5 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-white/20 pt-3">
|
||||
<div className="mb-3">
|
||||
<label className="text-white/60 text-xs mb-1 block">Дата поставки:</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={deliveryDate}
|
||||
onChange={(e) => setDeliveryDate(e.target.value)}
|
||||
className="bg-white/10 border-white/20 text-white h-8 text-sm"
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Выбор логистики */}
|
||||
<div className="mb-3">
|
||||
<label className="text-white/60 text-xs mb-1 block">Логистика (опционально):</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedLogistics?.id || ''}
|
||||
onChange={(e) => {
|
||||
const logisticsId = e.target.value
|
||||
const logistics = logisticsPartners.find((p: any) => p.id === logisticsId)
|
||||
setSelectedLogistics(logistics || null)
|
||||
}}
|
||||
className="w-full bg-white/10 border border-white/20 rounded-md px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent appearance-none"
|
||||
>
|
||||
<option value="" className="bg-gray-800 text-white">
|
||||
Выберите логистику
|
||||
</option>
|
||||
{logisticsPartners.map((partner: any) => (
|
||||
<option key={partner.id} value={partner.id} className="bg-gray-800 text-white">
|
||||
{partner.name || partner.fullName || partner.inn}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg className="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Заметки */}
|
||||
<div className="mb-3">
|
||||
<label className="text-white/60 text-xs mb-1 block">Заметки (необязательно):</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Дополнительная информация о поставке"
|
||||
rows={3}
|
||||
className="w-full bg-white/10 border border-white/20 rounded-md px-3 py-2 text-white text-sm placeholder-white/40 focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-white font-semibold text-sm">Итого:</span>
|
||||
<span className="text-green-400 font-bold text-lg">{formatCurrency(getTotalAmount())}</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCreateSupply}
|
||||
disabled={
|
||||
isCreatingSupply || !deliveryDate || selectedConsumables.length === 0
|
||||
}
|
||||
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white disabled:opacity-50 h-8 text-sm"
|
||||
>
|
||||
{isCreatingSupply ? 'Создание...' : 'Создать поставку'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,308 @@
|
||||
'use client'
|
||||
|
||||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import { Calendar, Plus, Trash2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { CREATE_FULFILLMENT_CONSUMABLE_SUPPLY } from '@/graphql/queries/fulfillment-consumables-v2'
|
||||
import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_PRODUCTS } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
article: string
|
||||
price: number
|
||||
quantity: number
|
||||
type: string
|
||||
}
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
name: string
|
||||
inn: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface SupplyItem {
|
||||
productId: string
|
||||
requestedQuantity: number
|
||||
product?: Product
|
||||
}
|
||||
|
||||
export default function CreateFulfillmentConsumablesSupplyV2Page() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [selectedSupplierId, setSelectedSupplierId] = useState('')
|
||||
const [requestedDeliveryDate, setRequestedDeliveryDate] = useState('')
|
||||
const [notes, setNotes] = useState('')
|
||||
const [items, setItems] = useState<SupplyItem[]>([])
|
||||
|
||||
// Получаем список контрагентов-поставщиков
|
||||
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery<{
|
||||
myCounterparties: Organization[]
|
||||
}>(GET_MY_COUNTERPARTIES)
|
||||
|
||||
// Получаем товары выбранного поставщика
|
||||
const { data: productsData, loading: productsLoading } = useQuery<{
|
||||
organizationProducts: Product[]
|
||||
}>(GET_ORGANIZATION_PRODUCTS, {
|
||||
variables: { organizationId: selectedSupplierId, type: 'CONSUMABLE' },
|
||||
skip: !selectedSupplierId,
|
||||
})
|
||||
|
||||
const [createSupply, { loading: creating }] = useMutation(CREATE_FULFILLMENT_CONSUMABLE_SUPPLY, {
|
||||
onCompleted: (data) => {
|
||||
if (data.createFulfillmentConsumableSupply.success) {
|
||||
toast.success('Поставка успешно создана')
|
||||
router.push('/fulfillment-supplies')
|
||||
} else {
|
||||
toast.error(data.createFulfillmentConsumableSupply.message)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
|
||||
const suppliers = counterpartiesData?.myCounterparties.filter(
|
||||
(org) => org.type === 'WHOLESALE'
|
||||
) || []
|
||||
|
||||
const consumableProducts = productsData?.organizationProducts || []
|
||||
|
||||
const addItem = () => {
|
||||
setItems([...items, { productId: '', requestedQuantity: 1 }])
|
||||
}
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
setItems(items.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const updateItem = (index: number, field: keyof SupplyItem, value: string | number) => {
|
||||
const newItems = [...items]
|
||||
if (field === 'productId') {
|
||||
const product = consumableProducts.find(p => p.id === value)
|
||||
newItems[index] = { ...newItems[index], [field]: value, product }
|
||||
} else {
|
||||
newItems[index] = { ...newItems[index], [field]: value }
|
||||
}
|
||||
setItems(newItems)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Валидация
|
||||
if (!selectedSupplierId) {
|
||||
toast.error('Выберите поставщика')
|
||||
return
|
||||
}
|
||||
|
||||
if (!requestedDeliveryDate) {
|
||||
toast.error('Укажите желаемую дату доставки')
|
||||
return
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
toast.error('Добавьте хотя бы один товар')
|
||||
return
|
||||
}
|
||||
|
||||
const invalidItems = items.filter(item => !item.productId || item.requestedQuantity <= 0)
|
||||
if (invalidItems.length > 0) {
|
||||
toast.error('Заполните все товары корректно')
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем поставку
|
||||
createSupply({
|
||||
variables: {
|
||||
input: {
|
||||
supplierId: selectedSupplierId,
|
||||
requestedDeliveryDate,
|
||||
items: items.map(item => ({
|
||||
productId: item.productId,
|
||||
requestedQuantity: item.requestedQuantity,
|
||||
})),
|
||||
notes: notes || undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const totalAmount = items.reduce((sum, item) => {
|
||||
if (item.product) {
|
||||
return sum + (item.product.price * item.requestedQuantity)
|
||||
}
|
||||
return sum
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 max-w-4xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-white">Создать поставку расходников ФФ (v2)</h1>
|
||||
<p className="text-white/70 mt-2">Новая система поставок</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-6 bg-white/10 backdrop-blur-xl border border-white/20">
|
||||
<div className="space-y-6">
|
||||
{/* Выбор поставщика */}
|
||||
<div>
|
||||
<Label htmlFor="supplier" className="text-white font-medium">Поставщик</Label>
|
||||
<Select
|
||||
value={selectedSupplierId}
|
||||
onValueChange={setSelectedSupplierId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Выберите поставщика" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{counterpartiesLoading ? (
|
||||
<SelectItem value="loading" disabled>Загрузка...</SelectItem>
|
||||
) : suppliers.length === 0 ? (
|
||||
<SelectItem value="empty" disabled>Нет доступных поставщиков</SelectItem>
|
||||
) : (
|
||||
suppliers.map((supplier) => (
|
||||
<SelectItem key={supplier.id} value={supplier.id}>
|
||||
{supplier.name} (ИНН: {supplier.inn})
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Дата доставки */}
|
||||
<div>
|
||||
<Label htmlFor="deliveryDate" className="text-white font-medium">Желаемая дата доставки</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
id="deliveryDate"
|
||||
type="date"
|
||||
value={requestedDeliveryDate}
|
||||
onChange={(e) => setRequestedDeliveryDate(e.target.value)}
|
||||
className="pl-10"
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Товары */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label className="text-white font-medium">Товары</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addItem}
|
||||
disabled={!selectedSupplierId || productsLoading}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Добавить товар
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center py-8 text-white/60 border-2 border-dashed border-white/20 rounded-lg">
|
||||
{selectedSupplierId ? 'Нажмите "Добавить товар" для начала' : 'Сначала выберите поставщика'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className="flex gap-3 items-end">
|
||||
<div className="flex-1">
|
||||
<Label className="text-white font-medium">Товар</Label>
|
||||
<Select
|
||||
value={item.productId}
|
||||
onValueChange={(value) => updateItem(index, 'productId', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Выберите товар" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{consumableProducts.map((product) => (
|
||||
<SelectItem key={product.id} value={product.id}>
|
||||
{product.name} ({product.article}) - {product.price} ₽
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="w-32">
|
||||
<Label className="text-white font-medium">Количество</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={item.requestedQuantity}
|
||||
onChange={(e) => updateItem(index, 'requestedQuantity', parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeItem(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Заметки */}
|
||||
<div>
|
||||
<Label htmlFor="notes" className="text-white font-medium">Заметки (необязательно)</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Дополнительная информация о поставке"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Итого */}
|
||||
{items.length > 0 && (
|
||||
<div className="bg-white/10 rounded-lg p-4 border border-white/20">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-medium text-white">Итого:</span>
|
||||
<span className="text-2xl font-bold text-white">{totalAmount.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопки */}
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={creating || items.length === 0}
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
|
||||
>
|
||||
{creating ? 'Создание...' : 'Создать поставку'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -103,7 +103,7 @@ export function FulfillmentSuppliesDashboard() {
|
||||
{/* УРОВЕНЬ 2: Подтабы */}
|
||||
{activeTab === 'fulfillment' && (
|
||||
<div className="ml-4 mb-3">
|
||||
<div className="grid w-full grid-cols-4 bg-white/8 backdrop-blur border-white/20 h-9 rounded-lg p-1">
|
||||
<div className="grid w-full grid-cols-5 bg-white/8 backdrop-blur border-white/20 h-9 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveSubTab('goods')}
|
||||
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 ${
|
||||
@ -156,6 +156,18 @@ export function FulfillmentSuppliesDashboard() {
|
||||
<span className="hidden sm:inline">Возвраты с ПВЗ</span>
|
||||
<span className="sm:hidden">В</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSubTab('consumables-v2')}
|
||||
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 ${
|
||||
activeSubTab === 'consumables-v2'
|
||||
? 'bg-white/15 text-white border-white/20'
|
||||
: 'text-white/60 hover:text-white/80'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">ФФ v2</span>
|
||||
<span className="sm:hidden">V2</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -369,6 +381,13 @@ export function FulfillmentSuppliesDashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* КОНТЕНТ ДЛЯ НОВОЙ СИСТЕМЫ ПОСТАВОК V2 */}
|
||||
{activeTab === 'fulfillment' && activeSubTab === 'consumables-v2' && (
|
||||
<div className="h-full overflow-hidden">
|
||||
<div className="text-white/80">Контент V2 системы (удален)</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* КОНТЕНТ ДЛЯ МАРКЕТПЛЕЙСОВ */}
|
||||
{activeTab === 'marketplace' && (
|
||||
<div className="text-white/80">Содержимое поставок на маркетплейсы</div>
|
||||
|
@ -0,0 +1,309 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Building2, ShoppingCart, Package, Wrench, RotateCcw, Clock, FileText, CheckCircle, ChevronRight, Home } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { GET_PENDING_SUPPLIES_COUNT } from '@/graphql/queries'
|
||||
import { useRealtime } from '@/hooks/useRealtime'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
// Компонент для отображения бейджа с уведомлениями
|
||||
function NotificationBadge({ count }: { count: number }) {
|
||||
if (count === 0) return null
|
||||
|
||||
return (
|
||||
<div className="ml-1 bg-red-500 text-white text-xs font-bold rounded-full min-w-[16px] h-4 flex items-center justify-center px-1">
|
||||
{count > 99 ? '99+' : count}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Breadcrumbs компонент
|
||||
function Breadcrumbs({ pathname }: { pathname: string }) {
|
||||
const getBreadcrumbs = (path: string) => {
|
||||
const segments = path.split('/').filter(Boolean)
|
||||
|
||||
const breadcrumbs = [
|
||||
{ name: 'Главная', href: '/dashboard', icon: Home }
|
||||
]
|
||||
|
||||
if (segments[0] === 'fulfillment-supplies') {
|
||||
breadcrumbs.push({ name: 'Входящие поставки', href: '/fulfillment-supplies', icon: Building2 })
|
||||
|
||||
if (segments[1]) {
|
||||
const categoryMap: Record<string, string> = {
|
||||
'goods': 'Товары',
|
||||
'detailed-supplies': 'Расходники фулфилмента',
|
||||
'consumables': 'Расходники селлеров',
|
||||
'returns': 'Возвраты с ПВЗ'
|
||||
}
|
||||
|
||||
const category = categoryMap[segments[1]]
|
||||
if (category) {
|
||||
breadcrumbs.push({
|
||||
name: category,
|
||||
href: segments[2] ? `/fulfillment-supplies/${segments[1]}` : path,
|
||||
icon: Package
|
||||
})
|
||||
|
||||
if (segments[1] === 'goods' && segments[2]) {
|
||||
const subcategoryMap: Record<string, string> = {
|
||||
'new': 'Новые',
|
||||
'receiving': 'Приёмка',
|
||||
'received': 'Принято'
|
||||
}
|
||||
|
||||
const subcategory = subcategoryMap[segments[2]]
|
||||
if (subcategory) {
|
||||
breadcrumbs.push({ name: subcategory, href: path, icon: Clock })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
const breadcrumbs = getBreadcrumbs(pathname)
|
||||
|
||||
return (
|
||||
<nav className="flex items-center space-x-2 text-sm text-white/60 mb-4">
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<React.Fragment key={breadcrumb.href}>
|
||||
{index > 0 && <ChevronRight className="h-4 w-4" />}
|
||||
{index === breadcrumbs.length - 1 ? (
|
||||
<span className="text-white font-medium flex items-center gap-1">
|
||||
{breadcrumb.icon && <breadcrumb.icon className="h-4 w-4" />}
|
||||
{breadcrumb.name}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={breadcrumb.href}
|
||||
className="hover:text-white transition-colors flex items-center gap-1"
|
||||
>
|
||||
{breadcrumb.icon && <breadcrumb.icon className="h-4 w-4" />}
|
||||
{breadcrumb.name}
|
||||
</Link>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export function FulfillmentSuppliesLayout({ children }: { children: React.ReactNode }) {
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
const pathname = usePathname()
|
||||
|
||||
// Загружаем данные о непринятых поставках
|
||||
const { data: pendingData, error: pendingError, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
onError: (error) => {
|
||||
console.error('❌ GET_PENDING_SUPPLIES_COUNT Error:', error)
|
||||
},
|
||||
})
|
||||
|
||||
// Realtime: обновление бейджа
|
||||
useRealtime({
|
||||
onEvent: (evt) => {
|
||||
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
||||
refetchPending()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Логируем ошибку для диагностики
|
||||
React.useEffect(() => {
|
||||
if (pendingError) {
|
||||
console.error('🚨 Ошибка загрузки счетчиков поставок:', pendingError)
|
||||
}
|
||||
}, [pendingError])
|
||||
|
||||
// Для фулфилмента считаем только поставки, НЕ заявки на партнерство
|
||||
const pendingCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0
|
||||
const ourSupplyOrdersCount = pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0
|
||||
const sellerSupplyOrdersCount = pendingData?.pendingSuppliesCount?.sellerSupplyOrders || 0
|
||||
|
||||
// Определяем активные табы на основе pathname
|
||||
const getActiveTab = () => {
|
||||
if (pathname.startsWith('/fulfillment-supplies/goods')) return 'fulfillment'
|
||||
if (pathname.includes('/detailed-supplies')) return 'fulfillment'
|
||||
if (pathname.includes('/consumables')) return 'fulfillment'
|
||||
if (pathname.includes('/returns')) return 'fulfillment'
|
||||
return 'fulfillment'
|
||||
}
|
||||
|
||||
const getActiveSubTab = () => {
|
||||
if (pathname.startsWith('/fulfillment-supplies/goods')) return 'goods'
|
||||
if (pathname.includes('/detailed-supplies')) return 'detailed-supplies'
|
||||
if (pathname.includes('/consumables')) return 'consumables'
|
||||
if (pathname.includes('/returns')) return 'returns'
|
||||
return 'goods'
|
||||
}
|
||||
|
||||
const getActiveThirdTab = () => {
|
||||
if (pathname.includes('/goods/new')) return 'new'
|
||||
if (pathname.includes('/goods/receiving')) return 'receiving'
|
||||
if (pathname.includes('/goods/received')) return 'received'
|
||||
return 'new'
|
||||
}
|
||||
|
||||
const activeTab = getActiveTab()
|
||||
const activeSubTab = getActiveSubTab()
|
||||
const activeThirdTab = getActiveThirdTab()
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
|
||||
<div className="h-full w-full flex flex-col space-y-4">
|
||||
{/* Breadcrumbs */}
|
||||
<Breadcrumbs pathname={pathname} />
|
||||
|
||||
{/* БЛОК 1: ТАБЫ ВСЕХ УРОВНЕЙ */}
|
||||
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6">
|
||||
{/* УРОВЕНЬ 1: Главные табы */}
|
||||
<div className="mb-4">
|
||||
<div className="grid w-full grid-cols-2 bg-white/15 backdrop-blur border-white/30 rounded-xl h-11 p-2">
|
||||
<Link
|
||||
href="/fulfillment-supplies/goods/new"
|
||||
className={`flex items-center gap-2 text-sm font-semibold transition-all duration-200 rounded-lg px-3 ${
|
||||
activeTab === 'fulfillment'
|
||||
? 'bg-gradient-to-r from-purple-500/40 to-pink-500/40 text-white shadow-lg'
|
||||
: 'text-white/80 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Поставки на фулфилмент</span>
|
||||
<span className="sm:hidden">Фулфилмент</span>
|
||||
<NotificationBadge count={pendingCount} />
|
||||
</Link>
|
||||
<button
|
||||
className="flex items-center gap-2 text-sm font-semibold transition-all duration-200 rounded-lg px-3 text-white/40 cursor-not-allowed"
|
||||
disabled
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Поставки на маркетплейсы</span>
|
||||
<span className="sm:hidden">Маркетплейсы</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* УРОВЕНЬ 2: Подтабы */}
|
||||
{activeTab === 'fulfillment' && (
|
||||
<div className="ml-4 mb-3">
|
||||
<div className="grid w-full grid-cols-4 bg-white/8 backdrop-blur border-white/20 h-9 rounded-lg p-1">
|
||||
<Link
|
||||
href="/fulfillment-supplies/goods/new"
|
||||
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 ${
|
||||
activeSubTab === 'goods'
|
||||
? 'bg-white/15 text-white border-white/20'
|
||||
: 'text-white/60 hover:text-white/80'
|
||||
}`}
|
||||
>
|
||||
<Package className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Товар</span>
|
||||
<span className="sm:hidden">Т</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/fulfillment-supplies/detailed-supplies"
|
||||
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 relative ${
|
||||
activeSubTab === 'detailed-supplies'
|
||||
? 'bg-white/15 text-white border-white/20'
|
||||
: 'text-white/60 hover:text-white/80'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span className="hidden md:inline">Расходники фулфилмента</span>
|
||||
<span className="md:hidden hidden sm:inline">Фулфилмент</span>
|
||||
<span className="sm:hidden">Ф</span>
|
||||
<NotificationBadge count={ourSupplyOrdersCount} />
|
||||
</Link>
|
||||
<Link
|
||||
href="/fulfillment-supplies/consumables"
|
||||
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 relative ${
|
||||
activeSubTab === 'consumables'
|
||||
? 'bg-white/15 text-white border-white/20'
|
||||
: 'text-white/60 hover:text-white/80'
|
||||
}`}
|
||||
>
|
||||
<Wrench className="h-3 w-3" />
|
||||
<span className="hidden md:inline">Расходники селлеров</span>
|
||||
<span className="md:hidden hidden sm:inline">Селлеры</span>
|
||||
<span className="sm:hidden">С</span>
|
||||
<NotificationBadge count={sellerSupplyOrdersCount} />
|
||||
</Link>
|
||||
<Link
|
||||
href="/fulfillment-supplies/returns"
|
||||
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 ${
|
||||
activeSubTab === 'returns'
|
||||
? 'bg-white/15 text-white border-white/20'
|
||||
: 'text-white/60 hover:text-white/80'
|
||||
}`}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Возвраты с ПВЗ</span>
|
||||
<span className="sm:hidden">В</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* УРОВЕНЬ 3: Подподтабы для товаров */}
|
||||
{activeTab === 'fulfillment' && activeSubTab === 'goods' && (
|
||||
<div className="ml-8">
|
||||
<div className="grid w-full grid-cols-3 bg-white/5 backdrop-blur border-white/15 h-8 rounded-md p-1">
|
||||
<Link
|
||||
href="/fulfillment-supplies/goods/new"
|
||||
className={`flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm px-2 ${
|
||||
activeThirdTab === 'new' ? 'bg-white/10 text-white' : 'text-white/50 hover:text-white/70'
|
||||
}`}
|
||||
>
|
||||
<Clock className="h-2.5 w-2.5" />
|
||||
<span className="hidden sm:inline">Новые</span>
|
||||
<span className="sm:hidden">Н</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/fulfillment-supplies/goods/receiving"
|
||||
className={`flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm px-2 ${
|
||||
activeThirdTab === 'receiving' ? 'bg-white/10 text-white' : 'text-white/50 hover:text-white/70'
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-2.5 w-2.5" />
|
||||
<span className="hidden sm:inline">Приёмка</span>
|
||||
<span className="sm:hidden">П</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/fulfillment-supplies/goods/received"
|
||||
className={`flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm px-2 ${
|
||||
activeThirdTab === 'received' ? 'bg-white/10 text-white' : 'text-white/50 hover:text-white/70'
|
||||
}`}
|
||||
>
|
||||
<CheckCircle className="h-2.5 w-2.5" />
|
||||
<span className="hidden sm:inline">Принято</span>
|
||||
<span className="sm:hidden">Пр</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* БЛОК 2: ОСНОВНОЙ КОНТЕНТ */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl h-full overflow-hidden p-6">
|
||||
<div className="h-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -207,34 +207,34 @@ export function FulfillmentConsumablesOrdersTab() {
|
||||
// Получаем данные заказов поставок
|
||||
const supplyOrders: SupplyOrder[] = data?.supplyOrders || []
|
||||
|
||||
// Фильтруем заказы для фулфилмента (ТОЛЬКО расходники селлеров)
|
||||
// Фильтруем заказы для фулфилмента (ТОЛЬКО расходники фулфилмента)
|
||||
const fulfillmentOrders = supplyOrders.filter((order) => {
|
||||
// Показываем только заказы где текущий фулфилмент-центр является получателем
|
||||
// Показываем только заказы созданные САМИМ фулфилментом для своих расходников
|
||||
const isCreatedBySelf = order.organization?.id === user?.organization?.id
|
||||
// И получатель тоже мы (фулфилмент заказывает расходники для себя)
|
||||
const isRecipient = order.fulfillmentCenter?.id === user?.organization?.id
|
||||
// НО создатель заказа НЕ мы (т.е. селлер создал заказ для нас)
|
||||
const isCreatedByOther = order.organization?.id !== user?.organization?.id
|
||||
// И статус не PENDING и не CANCELLED (одобренные поставщиком заявки)
|
||||
const isApproved = order.status !== 'CANCELLED' && order.status !== 'PENDING'
|
||||
// ✅ КРИТИЧНОЕ ИСПРАВЛЕНИЕ: Показывать только расходники селлеров (НЕ товары)
|
||||
const isSellerConsumables = order.consumableType === 'SELLER_CONSUMABLES'
|
||||
// ✅ КРИТИЧНОЕ ИСПРАВЛЕНИЕ: Показывать только расходники ФУЛФИЛМЕНТА (НЕ селлеров и НЕ товары)
|
||||
const isFulfillmentConsumables = order.consumableType === 'FULFILLMENT_CONSUMABLES'
|
||||
// Проверяем, что это НЕ товары (товары содержат услуги в рецептуре)
|
||||
const hasServices = order.items?.some(item => item.recipe?.services && item.recipe.services.length > 0)
|
||||
const isConsumablesOnly = isSellerConsumables && !hasServices
|
||||
const isConsumablesOnly = isFulfillmentConsumables && !hasServices
|
||||
|
||||
console.warn('🔍 Фильтрация расходников селлера:', {
|
||||
console.warn('🔍 Фильтрация расходников фулфилмента:', {
|
||||
orderId: order.id.slice(-8),
|
||||
isRecipient,
|
||||
isCreatedByOther,
|
||||
isCreatedBySelf,
|
||||
isApproved,
|
||||
isSellerConsumables,
|
||||
isFulfillmentConsumables,
|
||||
hasServices,
|
||||
isConsumablesOnly,
|
||||
consumableType: order.consumableType,
|
||||
itemsWithServices: order.items?.filter(item => item.recipe?.services && item.recipe.services.length > 0).length || 0,
|
||||
finalResult: isRecipient && isCreatedByOther && isApproved && isConsumablesOnly,
|
||||
finalResult: isRecipient && isCreatedBySelf && isApproved && isConsumablesOnly,
|
||||
})
|
||||
|
||||
return isRecipient && isCreatedByOther && isApproved && isConsumablesOnly
|
||||
return isRecipient && isCreatedBySelf && isApproved && isConsumablesOnly
|
||||
})
|
||||
|
||||
// Генерируем порядковые номера для заказов
|
||||
|
@ -1,120 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { TrendingUp, Wrench, Plus, Package2, Calendar } from 'lucide-react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { TrendingUp, Wrench, Plus, Package2, Clock, CheckCircle, XCircle, Truck } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { FULFILLMENT_RECEIVE_ORDER } from '@/graphql/mutations'
|
||||
import { GET_MY_SUPPLY_ORDERS, GET_MY_SUPPLIES, GET_WAREHOUSE_PRODUCTS } from '@/graphql/queries'
|
||||
import { GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES } from '@/graphql/queries/fulfillment-consumables-v2'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
import { MultiLevelSuppliesTable } from '../../supplies/multilevel-supplies-table'
|
||||
import { StatsCard } from '../../supplies/ui/stats-card'
|
||||
import { StatsGrid } from '../../supplies/ui/stats-grid'
|
||||
|
||||
// Интерфейс для заказа (совместимый с SupplyOrderFromGraphQL)
|
||||
interface SupplyOrder {
|
||||
// Интерфейс для новой системы поставок v2
|
||||
interface FulfillmentConsumableSupply {
|
||||
id: string
|
||||
organizationId: string
|
||||
partnerId: string
|
||||
deliveryDate: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
totalItems: number
|
||||
totalAmount: number
|
||||
status: string
|
||||
status: 'PENDING' | 'SUPPLIER_APPROVED' | 'LOGISTICS_CONFIRMED' | 'SHIPPED' | 'IN_TRANSIT' | 'DELIVERED' | 'CANCELLED'
|
||||
fulfillmentCenterId: string
|
||||
logisticsPartnerId?: string
|
||||
packagesCount?: number
|
||||
volume?: number
|
||||
responsibleEmployee?: string
|
||||
notes?: string
|
||||
number?: number // Порядковый номер
|
||||
organization: {
|
||||
fulfillmentCenter: {
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
market?: string
|
||||
}
|
||||
partner: {
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
name: string
|
||||
inn: string
|
||||
address?: string
|
||||
addressFull?: string
|
||||
market?: string
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
type: string
|
||||
}
|
||||
fulfillmentCenter?: {
|
||||
supplierId?: string
|
||||
supplier?: {
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
address?: string
|
||||
addressFull?: string
|
||||
type: string
|
||||
name: string
|
||||
inn: string
|
||||
}
|
||||
requestedDeliveryDate: string
|
||||
resalePricePerUnit?: number
|
||||
minStockLevel?: number
|
||||
notes?: string
|
||||
supplierApprovedAt?: string
|
||||
packagesCount?: number
|
||||
estimatedVolume?: number
|
||||
supplierContractId?: string
|
||||
supplierNotes?: string
|
||||
logisticsPartnerId?: string
|
||||
logisticsPartner?: {
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
name: string
|
||||
inn: string
|
||||
}
|
||||
routes: Array<{
|
||||
estimatedDeliveryDate?: string
|
||||
routeId?: string
|
||||
logisticsCost?: number
|
||||
logisticsNotes?: string
|
||||
shippedAt?: string
|
||||
trackingNumber?: string
|
||||
receivedAt?: string
|
||||
receivedById?: string
|
||||
receivedBy?: {
|
||||
id: string
|
||||
fromLocation: string
|
||||
toLocation: string
|
||||
fromAddress?: string
|
||||
toAddress?: string
|
||||
distance?: number
|
||||
estimatedTime?: number
|
||||
price?: number
|
||||
status?: string
|
||||
createdDate: string
|
||||
}>
|
||||
managerName: string
|
||||
phone: string
|
||||
}
|
||||
actualQuantity?: number
|
||||
defectQuantity?: number
|
||||
receiptNotes?: string
|
||||
items: Array<{
|
||||
id: string
|
||||
productId: string
|
||||
quantity: number
|
||||
price: number
|
||||
totalPrice: number
|
||||
product: {
|
||||
id: string
|
||||
name: string
|
||||
article: string
|
||||
description?: string
|
||||
category?: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
recipe?: {
|
||||
services?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
}>
|
||||
fulfillmentConsumables?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
}>
|
||||
sellerConsumables?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
}>
|
||||
marketplaceCardId?: string
|
||||
price: number
|
||||
quantity: number
|
||||
mainImage?: string
|
||||
}
|
||||
requestedQuantity: number
|
||||
approvedQuantity?: number
|
||||
shippedQuantity?: number
|
||||
receivedQuantity?: number
|
||||
defectQuantity?: number
|
||||
unitPrice: number
|
||||
totalPrice: number
|
||||
}>
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Функция для форматирования валюты
|
||||
@ -126,146 +93,110 @@ const formatCurrency = (amount: number) => {
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Функция для форматирования даты
|
||||
const _formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('ru-RU')
|
||||
}
|
||||
|
||||
// Функция для отображения статуса
|
||||
const _getStatusBadge = (status: string) => {
|
||||
// Функция для отображения статуса v2
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
PENDING: {
|
||||
label: 'Ожидает одобрения поставщика',
|
||||
color: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30',
|
||||
icon: Clock,
|
||||
},
|
||||
SUPPLIER_APPROVED: {
|
||||
label: 'Ожидает подтверждения логистики',
|
||||
label: 'Одобрено поставщиком',
|
||||
color: 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
LOGISTICS_CONFIRMED: {
|
||||
label: 'Ожидает отправки поставщиком',
|
||||
label: 'Логистика подтверждена',
|
||||
color: 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
|
||||
icon: Truck,
|
||||
},
|
||||
SHIPPED: {
|
||||
label: 'Отправлено',
|
||||
color: 'bg-orange-500/20 text-orange-300 border-orange-500/30',
|
||||
icon: Truck,
|
||||
},
|
||||
IN_TRANSIT: {
|
||||
label: 'В пути',
|
||||
color: 'bg-orange-500/20 text-orange-300 border-orange-500/30',
|
||||
icon: Truck,
|
||||
},
|
||||
DELIVERED: {
|
||||
label: 'Доставлено',
|
||||
color: 'bg-green-500/20 text-green-300 border-green-500/30',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
CANCELLED: {
|
||||
label: 'Отменено',
|
||||
color: 'bg-red-500/20 text-red-300 border-red-500/30',
|
||||
},
|
||||
// Устаревшие статусы для обратной совместимости
|
||||
CONFIRMED: {
|
||||
label: 'Подтверждён (устаревший)',
|
||||
color: 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||
},
|
||||
IN_TRANSIT: {
|
||||
label: 'В пути (устаревший)',
|
||||
color: 'bg-orange-500/20 text-orange-300 border-orange-500/30',
|
||||
icon: XCircle,
|
||||
},
|
||||
}
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.PENDING
|
||||
const IconComponent = config.icon
|
||||
|
||||
return <Badge className={config.color}>{config.label}</Badge>
|
||||
return (
|
||||
<Badge className={`${config.color} flex items-center gap-1`}>
|
||||
<IconComponent className="h-3 w-3" />
|
||||
{config.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
// Функция для форматирования даты
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function FulfillmentDetailedSuppliesTab() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(new Set())
|
||||
|
||||
// Убираем устаревшую мутацию updateSupplyOrderStatus
|
||||
|
||||
const [fulfillmentReceiveOrder] = useMutation(FULFILLMENT_RECEIVE_ORDER, {
|
||||
refetchQueries: [{ query: GET_MY_SUPPLY_ORDERS }, { query: GET_MY_SUPPLIES }, { query: GET_WAREHOUSE_PRODUCTS }],
|
||||
onCompleted: (data) => {
|
||||
if (data.fulfillmentReceiveOrder.success) {
|
||||
toast.success(data.fulfillmentReceiveOrder.message)
|
||||
} else {
|
||||
toast.error(data.fulfillmentReceiveOrder.message)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error receiving supply order:', error)
|
||||
toast.error('Ошибка при приеме заказа поставки')
|
||||
},
|
||||
})
|
||||
|
||||
// Загружаем реальные данные заказов расходников с многоуровневой структурой
|
||||
const { data, loading, error } = useQuery(GET_MY_SUPPLY_ORDERS, {
|
||||
fetchPolicy: 'cache-and-network', // Принудительно проверяем сервер
|
||||
// Загружаем поставки расходников через новый API v2
|
||||
const { data, loading, error, refetch } = useQuery(GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
errorPolicy: 'all',
|
||||
onError: (error) => {
|
||||
console.error('❌ GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES Error:', error)
|
||||
},
|
||||
})
|
||||
|
||||
// Получаем ID текущей организации (фулфилмент-центра)
|
||||
const currentOrganizationId = user?.organization?.id
|
||||
// Получаем поставки из нового API
|
||||
const supplies: FulfillmentConsumableSupply[] = data?.myFulfillmentConsumableSupplies || []
|
||||
|
||||
// Получаем поставки с многоуровневой структурой для фулфилмента
|
||||
// Фильтруем поставки где мы являемся получателем (фулфилмент-центром)
|
||||
// И это расходники фулфилмента (FULFILLMENT_CONSUMABLES)
|
||||
const ourSupplyOrders: SupplyOrder[] = (data?.mySupplyOrders || []).filter((order: SupplyOrder) => {
|
||||
// Проверяем что order существует и имеет нужные поля
|
||||
if (!order || !order.fulfillmentCenterId) return false
|
||||
|
||||
// Фильтруем только расходники фулфилмента
|
||||
const isFulfillmentConsumables = (order as any).consumableType === 'FULFILLMENT_CONSUMABLES'
|
||||
const isOurFulfillmentCenter = order.fulfillmentCenterId === currentOrganizationId
|
||||
|
||||
console.warn('🔍 Фильтрация расходников фулфилмента:', {
|
||||
orderId: order.id?.slice(-8),
|
||||
consumableType: (order as any).consumableType,
|
||||
isFulfillmentConsumables,
|
||||
isOurFulfillmentCenter,
|
||||
result: isFulfillmentConsumables && isOurFulfillmentCenter,
|
||||
})
|
||||
|
||||
return isFulfillmentConsumables && isOurFulfillmentCenter
|
||||
})
|
||||
|
||||
// Обработчик действий фулфилмента для многоуровневой таблицы
|
||||
const handleFulfillmentAction = async (supplyId: string, action: string) => {
|
||||
try {
|
||||
switch (action) {
|
||||
case 'accept':
|
||||
// Принять поставку от поставщика (переход из SUPPLIER_APPROVED в CONFIRMED)
|
||||
await fulfillmentReceiveOrder({ variables: { id: supplyId } })
|
||||
break
|
||||
case 'cancel':
|
||||
// Отменить поставку (если разрешено)
|
||||
console.warn('Отмена поставки:', supplyId)
|
||||
toast.info('Функция отмены поставки в разработке')
|
||||
break
|
||||
default:
|
||||
console.warn('Неизвестное действие фулфилмента:', action, supplyId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении действия фулфилмента:', error)
|
||||
toast.error('Ошибка при выполнении действия')
|
||||
// Функция для переключения развернутого состояния поставки
|
||||
const toggleExpanded = (supplyId: string) => {
|
||||
const newExpanded = new Set(expandedSupplies)
|
||||
if (newExpanded.has(supplyId)) {
|
||||
newExpanded.delete(supplyId)
|
||||
} else {
|
||||
newExpanded.add(supplyId)
|
||||
}
|
||||
setExpandedSupplies(newExpanded)
|
||||
}
|
||||
|
||||
// Функция для приема заказа фулфилментом
|
||||
const _handleReceiveOrder = async (orderId: string) => {
|
||||
try {
|
||||
await fulfillmentReceiveOrder({
|
||||
variables: { id: orderId },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error receiving order:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Убираем устаревшие функции проверки статусов
|
||||
// Вычисляем статистику
|
||||
const totalSupplies = supplies.length
|
||||
const totalAmount = supplies.reduce((sum, supply) => {
|
||||
return sum + supply.items.reduce((itemSum, item) => itemSum + item.totalPrice, 0)
|
||||
}, 0)
|
||||
const totalItems = supplies.reduce((sum, supply) => {
|
||||
return sum + supply.items.reduce((itemSum, item) => itemSum + item.requestedQuantity, 0)
|
||||
}, 0)
|
||||
const deliveredCount = supplies.filter(supply => supply.status === 'DELIVERED').length
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
|
||||
<span className="ml-3 text-white/60">Загрузка наших расходников...</span>
|
||||
<span className="ml-3 text-white/60">Загрузка расходников фулфилмента...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -277,6 +208,13 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
||||
<p className="text-red-400 font-medium">Ошибка загрузки расходников</p>
|
||||
<p className="text-white/60 text-sm mt-2">{error.message}</p>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
variant="outline"
|
||||
className="mt-4 border-white/20 text-white/80 hover:bg-white/10"
|
||||
>
|
||||
Повторить попытку
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -288,10 +226,10 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-1">Расходники фулфилмента</h2>
|
||||
<p className="text-white/60 text-sm">Поставки расходников, поступающие на склад фулфилмент-центра</p>
|
||||
<p className="text-white/60 text-sm">Поставки расходников для вашего фулфилмент-центра (система v2)</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => router.push('/fulfillment-supplies/create-consumables')}
|
||||
onClick={() => router.push('/supplies/create-fulfillment-consumables-v2')}
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-lg"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
@ -302,8 +240,8 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
{/* Статистика наших расходников */}
|
||||
<StatsGrid>
|
||||
<StatsCard
|
||||
title="Наши поставки"
|
||||
value={ourSupplyOrders.length}
|
||||
title="Всего поставок"
|
||||
value={totalSupplies}
|
||||
icon={Package2}
|
||||
iconColor="text-orange-400"
|
||||
iconBg="bg-orange-500/20"
|
||||
@ -312,9 +250,7 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
|
||||
<StatsCard
|
||||
title="Общая сумма"
|
||||
value={formatCurrency(
|
||||
ourSupplyOrders.reduce((sum: number, order: SupplyOrder) => sum + (order.totalAmount || 0), 0),
|
||||
)}
|
||||
value={formatCurrency(totalAmount)}
|
||||
icon={TrendingUp}
|
||||
iconColor="text-green-400"
|
||||
iconBg="bg-green-500/20"
|
||||
@ -323,7 +259,7 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
|
||||
<StatsCard
|
||||
title="Всего единиц"
|
||||
value={ourSupplyOrders.reduce((sum: number, order: SupplyOrder) => sum + (order.totalItems || 0), 0)}
|
||||
value={totalItems}
|
||||
icon={Wrench}
|
||||
iconColor="text-blue-400"
|
||||
iconBg="bg-blue-500/20"
|
||||
@ -331,37 +267,136 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Завершено"
|
||||
value={ourSupplyOrders.filter((order: SupplyOrder) => order.status === 'DELIVERED').length}
|
||||
icon={Calendar}
|
||||
title="Доставлено"
|
||||
value={deliveredCount}
|
||||
icon={CheckCircle}
|
||||
iconColor="text-purple-400"
|
||||
iconBg="bg-purple-500/20"
|
||||
subtitle="Доставленные поставки"
|
||||
subtitle="Завершенные поставки"
|
||||
/>
|
||||
</StatsGrid>
|
||||
|
||||
{/* Многоуровневая таблица поставок для фулфилмента */}
|
||||
{ourSupplyOrders.length === 0 ? (
|
||||
{/* Список поставок */}
|
||||
{supplies.length === 0 ? (
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
|
||||
<div className="text-center">
|
||||
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Пока нет поставок расходников</h3>
|
||||
<p className="text-white/60">
|
||||
Здесь будут отображаться поставки расходников, поступающие на ваш склад. Создайте заказ через кнопку
|
||||
"Создать поставку" или ожидайте поставки от партнеров.
|
||||
Здесь будут отображаться поставки расходников для вашего фулфилмент-центра.
|
||||
Создайте новую поставку через кнопку "Создать поставку".
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden p-6">
|
||||
<MultiLevelSuppliesTable
|
||||
supplies={ourSupplyOrders as any}
|
||||
userRole="FULFILLMENT"
|
||||
onSupplyAction={handleFulfillmentAction}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
<div className="space-y-4">
|
||||
{supplies.map((supply) => (
|
||||
<Card key={supply.id} className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
|
||||
{/* Основная информация о поставке */}
|
||||
<div
|
||||
className="p-6 cursor-pointer hover:bg-white/5 transition-colors"
|
||||
onClick={() => toggleExpanded(supply.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Поставка #{supply.id.slice(-8)}
|
||||
</h3>
|
||||
{getStatusBadge(supply.status)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-white/60">Поставщик</p>
|
||||
<p className="text-white font-medium">{supply.supplier?.name || 'Не указан'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Дата поставки</p>
|
||||
<p className="text-white">{formatDate(supply.requestedDeliveryDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Товаров</p>
|
||||
<p className="text-white">{supply.items.length}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Сумма</p>
|
||||
<p className="text-white font-medium">
|
||||
{formatCurrency(supply.items.reduce((sum, item) => sum + item.totalPrice, 0))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<Clock className={`h-5 w-5 transition-transform ${
|
||||
expandedSupplies.has(supply.id) ? 'rotate-180' : ''
|
||||
} text-white/60`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Развернутая информация */}
|
||||
{expandedSupplies.has(supply.id) && (
|
||||
<div className="border-t border-white/20 p-6 bg-white/5">
|
||||
{/* Товары в поставке */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-white font-semibold mb-4">Товары в поставке</h4>
|
||||
<div className="space-y-3">
|
||||
{supply.items.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-4 p-4 bg-white/5 rounded-lg">
|
||||
{item.product.mainImage && (
|
||||
<img
|
||||
src={item.product.mainImage}
|
||||
alt={item.product.name}
|
||||
className="h-12 w-12 object-cover rounded-lg"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h5 className="text-white font-medium">{item.product.name}</h5>
|
||||
<p className="text-white/60 text-sm">Артикул: {item.product.article}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white">Количество: {item.requestedQuantity}</p>
|
||||
<p className="text-white/60 text-sm">
|
||||
{formatCurrency(item.unitPrice)} × {item.requestedQuantity} = {formatCurrency(item.totalPrice)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Дополнительная информация */}
|
||||
{(supply.notes || supply.supplierNotes || supply.logisticsNotes) && (
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4">Дополнительная информация</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{supply.notes && (
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<h5 className="text-white/80 font-medium mb-2">Заметки ФФ</h5>
|
||||
<p className="text-white/60 text-sm">{supply.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
{supply.supplierNotes && (
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<h5 className="text-white/80 font-medium mb-2">Заметки поставщика</h5>
|
||||
<p className="text-white/60 text-sm">{supply.supplierNotes}</p>
|
||||
</div>
|
||||
)}
|
||||
{supply.logisticsNotes && (
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<h5 className="text-white/80 font-medium mb-2">Заметки логистики</h5>
|
||||
<p className="text-white/60 text-sm">{supply.logisticsNotes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import {
|
||||
XCircle,
|
||||
Hash,
|
||||
Settings,
|
||||
Truck,
|
||||
} from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { SUPPLIER_APPROVE_ORDER, SUPPLIER_REJECT_ORDER, SUPPLIER_SHIP_ORDER, UPDATE_SUPPLY_PARAMETERS } from '@/graphql/mutations'
|
||||
import { GET_MY_SUPPLY_ORDERS } from '@/graphql/queries'
|
||||
import { GET_MY_SUPPLIER_CONSUMABLE_SUPPLIES } from '@/graphql/queries/fulfillment-consumables-v2'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
|
||||
@ -126,6 +127,11 @@ export function SupplierOrdersTabs() {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Загружаем новые заявки v2 на расходники фулфилмента
|
||||
const { data: v2Data, loading: v2Loading, error: v2Error } = useQuery(GET_MY_SUPPLIER_CONSUMABLE_SUPPLIES, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Мутации для действий поставщика
|
||||
const [supplierApproveOrder] = useMutation(SUPPLIER_APPROVE_ORDER, {
|
||||
refetchQueries: [{ query: GET_MY_SUPPLY_ORDERS }],
|
||||
@ -438,6 +444,19 @@ export function SupplierOrdersTabs() {
|
||||
<Badge className="ml-2 bg-white/20 text-white/70 border-white/30">{getTabBadgeCount('all')}</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value="consumables-v2"
|
||||
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
|
||||
>
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
Расходники v2
|
||||
{v2Data?.mySupplierConsumableSupplies.length > 0 && (
|
||||
<Badge className="ml-2 bg-blue-500/20 text-blue-300 border-blue-400/30">
|
||||
{v2Data.mySupplierConsumableSupplies.length}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
@ -452,30 +471,79 @@ export function SupplierOrdersTabs() {
|
||||
onDateFilterChange={setDateFilter}
|
||||
/>
|
||||
|
||||
{/* Многоуровневая таблица поставок для поставщика */}
|
||||
{/* Отображение контента */}
|
||||
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl">
|
||||
<div className="p-6">
|
||||
{getCurrentOrders().length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
{activeTab === 'new' ? 'Нет новых заявок' : 'Заявки не найдены'}
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
{activeTab === 'new'
|
||||
? 'Новые заявки от заказчиков будут отображаться здесь'
|
||||
: 'Попробуйте изменить фильтры поиска'}
|
||||
</p>
|
||||
</div>
|
||||
{activeTab === 'consumables-v2' ? (
|
||||
// Отображение новых заявок v2
|
||||
v2Data?.mySupplierConsumableSupplies.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Нет заявок на расходники v2
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
Заявки на расходники от фулфилмент-центров будут отображаться здесь
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">
|
||||
Заявки на расходники v2 ({v2Data?.mySupplierConsumableSupplies.length || 0})
|
||||
</h3>
|
||||
{v2Data?.mySupplierConsumableSupplies.map((supply: any) => (
|
||||
<div key={supply.id} className="bg-white/5 rounded-lg p-4 border border-white/10">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h4 className="font-medium text-white">
|
||||
Заявка #{supply.id.slice(-8)}
|
||||
</h4>
|
||||
<p className="text-white/60 text-sm">
|
||||
От: {supply.fulfillmentCenter.name}
|
||||
</p>
|
||||
</div>
|
||||
<Badge className={
|
||||
supply.status === 'PENDING' ? 'bg-yellow-100 text-yellow-800' :
|
||||
supply.status === 'SUPPLIER_APPROVED' ? 'bg-green-100 text-green-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}>
|
||||
{supply.status === 'PENDING' ? 'Ожидает одобрения' :
|
||||
supply.status === 'SUPPLIER_APPROVED' ? 'Одобрено' : supply.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-white/80 text-sm">
|
||||
<p>Дата доставки: {new Date(supply.requestedDeliveryDate).toLocaleDateString('ru-RU')}</p>
|
||||
<p>Товаров: {supply.items.length}</p>
|
||||
{supply.notes && <p>Заметки: {supply.notes}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<MultiLevelSuppliesTable
|
||||
supplies={getCurrentOrders()}
|
||||
userRole="WHOLESALE"
|
||||
activeTab={activeTab}
|
||||
onSupplyAction={handleSupplierAction}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
onPackagesChange={handlePackagesChange}
|
||||
/>
|
||||
// Обычные заявки (существующая логика)
|
||||
getCurrentOrders().length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
{activeTab === 'new' ? 'Нет новых заявок' : 'Заявки не найдены'}
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
{activeTab === 'new'
|
||||
? 'Новые заявки от заказчиков будут отображаться здесь'
|
||||
: 'Попробуйте изменить фильтры поиска'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<MultiLevelSuppliesTable
|
||||
supplies={getCurrentOrders()}
|
||||
userRole="WHOLESALE"
|
||||
activeTab={activeTab}
|
||||
onSupplyAction={handleSupplierAction}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
onPackagesChange={handlePackagesChange}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -738,8 +738,13 @@ export function MultiLevelSuppliesTable({
|
||||
)}
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Цена товаров</TableHead>
|
||||
{/* 🆕 НОВЫЕ КОЛОНКИ: Объём и грузовые места между "Цена товаров" и "Статус" */}
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Объём</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Грузовые места</TableHead>
|
||||
{/* 🔒 ВИДИМОСТЬ: Только для WHOLESALE, FULFILLMENT, LOGIST (НЕ для SELLER) */}
|
||||
{(userRole === 'WHOLESALE' || userRole === 'FULFILLMENT' || userRole === 'LOGIST') && (
|
||||
<>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Объём</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Грузовые места</TableHead>
|
||||
</>
|
||||
)}
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит услуги ФФ, расходники и логистику */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap hidden lg:table-cell">
|
||||
@ -844,73 +849,78 @@ export function MultiLevelSuppliesTable({
|
||||
</span>
|
||||
</TableCell>
|
||||
{/* 🆕 НОВЫЕ ЯЧЕЙКИ: Объём и грузовые места с инпутами для WHOLESALE */}
|
||||
<TableCell>
|
||||
{userRole === 'WHOLESALE' && supply.status === 'PENDING' ? (
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.0 м³"
|
||||
value={inputValues[supply.id]?.volume ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
// Устанавливаем pending состояние
|
||||
setPendingUpdates(prev => new Set(prev).add(`${supply.id}-volume`))
|
||||
// Обновляем локальное состояние немедленно
|
||||
setInputValues(prev => ({
|
||||
...prev,
|
||||
[supply.id]: {
|
||||
...prev[supply.id],
|
||||
volume: value,
|
||||
},
|
||||
}))
|
||||
// Вызываем обработчик с преобразованным значением
|
||||
const numValue = value === '' ? null : parseFloat(value)
|
||||
onVolumeChange?.(supply.id, numValue)
|
||||
}}
|
||||
onFocus={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="w-20 bg-white/10 border border-white/20 rounded px-2 py-1 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-white/40"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80 text-sm">
|
||||
{supply.volume ? `${supply.volume} м³` : '—'}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{userRole === 'WHOLESALE' && supply.status === 'PENDING' ? (
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0 мест"
|
||||
value={inputValues[supply.id]?.packages ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
// Устанавливаем pending состояние
|
||||
setPendingUpdates(prev => new Set(prev).add(`${supply.id}-packages`))
|
||||
// Обновляем локальное состояние немедленно
|
||||
setInputValues(prev => ({
|
||||
...prev,
|
||||
[supply.id]: {
|
||||
...prev[supply.id],
|
||||
packages: value,
|
||||
},
|
||||
}))
|
||||
// Вызываем обработчик с преобразованным значением
|
||||
const numValue = value === '' ? null : parseInt(value)
|
||||
onPackagesChange?.(supply.id, numValue)
|
||||
}}
|
||||
onFocus={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="w-20 bg-white/10 border border-white/20 rounded px-2 py-1 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-white/40"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80 text-sm">
|
||||
{supply.packagesCount ? `${supply.packagesCount} мест` : '—'}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* 🔒 ВИДИМОСТЬ: Только для WHOLESALE, FULFILLMENT, LOGIST (НЕ для SELLER) */}
|
||||
{(userRole === 'WHOLESALE' || userRole === 'FULFILLMENT' || userRole === 'LOGIST') && (
|
||||
<>
|
||||
<TableCell>
|
||||
{userRole === 'WHOLESALE' && supply.status === 'PENDING' ? (
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.0 м³"
|
||||
value={inputValues[supply.id]?.volume ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
// Устанавливаем pending состояние
|
||||
setPendingUpdates(prev => new Set(prev).add(`${supply.id}-volume`))
|
||||
// Обновляем локальное состояние немедленно
|
||||
setInputValues(prev => ({
|
||||
...prev,
|
||||
[supply.id]: {
|
||||
...prev[supply.id],
|
||||
volume: value,
|
||||
},
|
||||
}))
|
||||
// Вызываем обработчик с преобразованным значением
|
||||
const numValue = value === '' ? null : parseFloat(value)
|
||||
onVolumeChange?.(supply.id, numValue)
|
||||
}}
|
||||
onFocus={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="w-20 bg-white/10 border border-white/20 rounded px-2 py-1 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-white/40"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80 text-sm">
|
||||
{supply.volume ? `${supply.volume} м³` : '—'}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{userRole === 'WHOLESALE' && supply.status === 'PENDING' ? (
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0 мест"
|
||||
value={inputValues[supply.id]?.packages ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
// Устанавливаем pending состояние
|
||||
setPendingUpdates(prev => new Set(prev).add(`${supply.id}-packages`))
|
||||
// Обновляем локальное состояние немедленно
|
||||
setInputValues(prev => ({
|
||||
...prev,
|
||||
[supply.id]: {
|
||||
...prev[supply.id],
|
||||
packages: value,
|
||||
},
|
||||
}))
|
||||
// Вызываем обработчик с преобразованным значением
|
||||
const numValue = value === '' ? null : parseInt(value)
|
||||
onPackagesChange?.(supply.id, numValue)
|
||||
}}
|
||||
onFocus={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="w-20 bg-white/10 border border-white/20 rounded px-2 py-1 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-white/40"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80 text-sm">
|
||||
{supply.packagesCount ? `${supply.packagesCount} мест` : '—'}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит услуги ФФ, расходники и логистику */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
|
252
src/graphql/queries/fulfillment-consumables-v2.ts
Normal file
252
src/graphql/queries/fulfillment-consumables-v2.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@ -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
|
||||
|
268
src/graphql/resolvers/fulfillment-consumables-v2.ts
Normal file
268
src/graphql/resolvers/fulfillment-consumables-v2.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
@ -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 безопасности ко всем резолверам
|
||||
|
@ -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!
|
||||
}
|
||||
`
|
||||
|
Reference in New Issue
Block a user