
- Обновлена форма создания поставок расходников фулфилмента для использования 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>
33 KiB
33 KiB
🗄️ СХЕМА БАЗЫ ДАННЫХ SFERA v2.0 - СИСТЕМА ПОСТАВОК
⚠️ ВАЖНО: Этот документ описывает НОВЫЕ таблицы для системы поставок v2.0. Существующие таблицы остаются без изменений для обратной совместимости.
📦 НОВЫЕ ТАБЛИЦЫ ПОСТАВОК НА ФУЛФИЛМЕНТ
1️⃣ FulfillmentConsumableSupplyOrder - Поставки расходников ФФ
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 - Поставки расходников селлеров
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 - Товарные поставки
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
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
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 - Статусы поставок НА фулфилмент
enum SupplyOrderStatusV2 {
PENDING // Ожидает одобрения поставщика
SUPPLIER_APPROVED // Одобрено поставщиком
LOGISTICS_CONFIRMED // Логистика подтверждена
SHIPPED // Отгружено поставщиком
IN_TRANSIT // В пути
DELIVERED // Доставлено и принято
CANCELLED // Отменено
}
MarketplaceSupplyStatus - Статусы поставок НА маркетплейсы
enum MarketplaceSupplyStatus {
PLANNED // Запланирована
PREPARED // Подготовлена к отгрузке
SHIPPED_TO_MARKETPLACE // Отгружена на маркетплейс
ACCEPTED_BY_MARKETPLACE // Принята маркетплейсом
CANCELLED // Отменена
}
SellerConsumableAccessRights - Права доступа к расходникам селлеров
enum SellerConsumableAccessRights {
SELLER_ONLY // Только селлер может использовать
SHARED_WITH_FF // ФФ тоже может использовать (с разрешения)
}
🔄 ОБНОВЛЕНИЯ СУЩЕСТВУЮЩИХ ТАБЛИЦ
Product - Добавление новых связей
model Product {
// ... существующие поля остаются без изменений
// === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 ===
fulfillmentSupplyItems FulfillmentConsumableSupplyItem[] @relation("FFSupplyItems")
sellerSupplyItems SellerConsumableSupplyItem[] @relation("SellerSupplyItems")
goodsSupplyItems GoodsSupplyItem[] @relation("GoodsSupplyItems")
ozonSupplyItems OzonSupplyItem[] @relation("OzonSupplyItems")
wildberriesSupplyItems WildberriesSupplyItem[] @relation("WBSupplyItems")
// ... остальные поля остаются без изменений
}
Organization - Добавление новых связей
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 - Добавление новых связей
model User {
// ... существующие поля остаются без изменений
// === НОВЫЕ СВЯЗИ С ПРИЕМКОЙ ПОСТАВОК V2 ===
fulfillmentSupplyOrdersReceived FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersReceiver")
sellerSupplyOrdersReceived SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersReceiver")
goodsSupplyOrdersReceived GoodsSupplyOrder[] @relation("GoodsSupplyOrdersReceiver")
// ... остальные поля остаются без изменений
}
🔍 ИНДЕКСЫ ДЛЯ ПРОИЗВОДИТЕЛЬНОСТИ
// Индексы для быстрого поиска поставок по статусу и организации
@@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: Создание новых таблиц
-- Создание таблиц параллельно с существующими
CREATE TABLE fulfillment_consumable_supply_orders (...);
CREATE TABLE fulfillment_consumable_supply_items (...);
-- и т.д.
Phase 2: Новые enum значения
-- Добавление новых enum без затрагивания старых
CREATE TYPE "SupplyOrderStatusV2" AS ENUM (...);
CREATE TYPE "MarketplaceSupplyStatus" AS ENUM (...);
Phase 3: Обновление связей (без удаления старых)
-- Добавление новых foreign key связей
ALTER TABLE "Product" ADD COLUMN ...;
ALTER TABLE "Organization" ADD COLUMN ...;
Phase 4: Данные миграции (только с одобрения)
-- Миграция данных из старых таблиц в новые
-- ТОЛЬКО после полного тестирования новой системы
⚠️ ВАЖНЫЕ ПРИНЦИПЫ
✅ ОБРАТНАЯ СОВМЕСТИМОСТЬ
- Все существующие таблицы остаются без изменений
- Новые таблицы создаются параллельно
- Старая система продолжает работать
🔒 БЕЗОПАСНОСТЬ ДАННЫХ
- Каждый тип поставки изолирован в отдельной таблице
- Связи защищены foreign key constraints
- Каскадное удаление только для зависимых записей
📈 МАСШТАБИРУЕМОСТЬ
- Легко добавлять новые типы поставок
- Оптимизированные индексы для каждого use case
- Независимые схемы для разных процессов
🛡️ ЦЕЛОСТНОСТЬ ДАННЫХ
- Строгие ограничения на связи между таблицами
- Валидация через enum значения
- Уникальные индексы предотвращают дублирование
Следующий шаг: Создание Prisma миграций для FulfillmentConsumableSupplyOrder