From ec2880354962c882ca9d14713a380fd8215e308f Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Sun, 27 Jul 2025 20:10:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B1=D0=B8=D0=B7=D0=BD=D0=B5=D1=81-=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=BE=D0=B2=20=D0=B2=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B5=20=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F.=20?= =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20UIKitSection=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=BC=D0=BE=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8.=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BA=D0=BE=D0=B4=D0=B0.=20=D0=98=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BC=D0=B5=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B2=D1=8B=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=83=D0=B4=D0=BE=D0=B1=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B0=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/ui-kit-section.tsx | 11 + .../admin/ui-kit/business-processes-demo.tsx | 1147 +++++++++++++ .../admin/ui-kit/supplies-navigation-demo.tsx | 2 +- src/components/dashboard/sidebar.tsx | 124 +- .../fulfillment-consumables-orders-tab.tsx | 855 ++++++---- .../fulfillment-warehouse-dashboard.tsx | 1432 ++++++++++++----- .../consumables-supplies-tab.tsx | 13 +- .../supplies/create-supply-page.tsx | 140 +- .../supplies/direct-supply-creation.tsx | 6 +- .../fulfillment-supplies-tab.tsx | 8 +- .../real-supply-orders-tab.tsx | 307 +++- .../seller-supply-orders-tab.tsx | 418 +++++ .../wb-warehouse/stock-table-row.tsx | 199 ++- .../wb-warehouse-dashboard-refactored.tsx | 415 ++--- src/graphql/mutations.ts | 205 ++- src/graphql/resolvers.ts | 226 ++- src/graphql/typedefs.ts | 1 + 17 files changed, 4304 insertions(+), 1205 deletions(-) create mode 100644 src/components/admin/ui-kit/business-processes-demo.tsx create mode 100644 src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx diff --git a/src/components/admin/ui-kit-section.tsx b/src/components/admin/ui-kit-section.tsx index 48625dd..2bbcf88 100644 --- a/src/components/admin/ui-kit-section.tsx +++ b/src/components/admin/ui-kit-section.tsx @@ -21,6 +21,7 @@ import { FulfillmentWarehouse2Demo } from "./ui-kit/fulfillment-warehouse-2-demo import { SuppliesDemo } from "./ui-kit/supplies-demo"; import { WBWarehouseDemo } from "./ui-kit/wb-warehouse-demo"; import { SuppliesNavigationDemo } from "./ui-kit/supplies-navigation-demo"; +import { BusinessProcessesDemo } from "./ui-kit/business-processes-demo"; export function UIKitSection() { return ( @@ -154,6 +155,12 @@ export function UIKitSection() { > Навигация поставок + + Бизнес-процессы + @@ -235,6 +242,10 @@ export function UIKitSection() { + + + + ); diff --git a/src/components/admin/ui-kit/business-processes-demo.tsx b/src/components/admin/ui-kit/business-processes-demo.tsx new file mode 100644 index 0000000..1d015e0 --- /dev/null +++ b/src/components/admin/ui-kit/business-processes-demo.tsx @@ -0,0 +1,1147 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + ShoppingCart, + Package, + Truck, + Warehouse, + CheckCircle, + Clock, + AlertCircle, + ArrowRight, + Users, + FileText, + MapPin, + DollarSign, + MessageSquare, + Building2, + Settings, + Handshake, + MessageCircle, + Store, +} from "lucide-react"; + +export function BusinessProcessesDemo() { + const processSteps = [ + { + id: 1, + title: "Селлер заходит в маркет", + description: "Пользователь входит в систему и переходит в раздел маркета", + actor: "Селлер", + status: "completed", + icon: ShoppingCart, + location: "Кабинет селлера → Маркет", + }, + { + id: 2, + title: "Выбор категории", + description: "Селлер выбирает нужную категорию товаров", + actor: "Селлер", + status: "completed", + icon: Package, + location: "Маркет → Категории", + }, + { + id: 3, + title: "Выбор товара", + description: "Выбор конкретного товара из каталога", + actor: "Селлер", + status: "completed", + icon: Package, + location: "Каталог товаров", + }, + { + id: 4, + title: "Заказ количества", + description: "Указание необходимого количества товара", + actor: "Селлер", + status: "completed", + icon: FileText, + location: "Форма заказа", + }, + { + id: 5, + title: "Выбор услуг фулфилмента", + description: "Выбор услуг фулфилмента и расходников для каждого товара", + actor: "Селлер", + status: "completed", + icon: Warehouse, + location: "Настройки заказа", + }, + { + id: 6, + title: "Создание заявки", + description: + "Нажатие кнопки 'Создать заявку' и формирование заявки на поставку", + actor: "Селлер", + status: "in-progress", + icon: FileText, + location: "Форма заказа", + }, + { + id: 7, + title: "Сохранение в кабинете селлера", + description: + "Заявка сохраняется в разделе 'Мои поставки' → 'Поставки на ФФ' → 'Товар'", + actor: "Система", + status: "pending", + icon: Package, + location: "Кабинет селлера → Мои поставки", + }, + { + id: 8, + title: "Дублирование поставщику", + description: "Заявка дублируется в кабинет поставщика чей товар заказали", + actor: "Система", + status: "pending", + icon: Users, + location: "Кабинет поставщика → Заявки", + }, + { + id: 9, + title: "Одобрение поставщиком", + description: "Поставщик должен одобрить заявку на поставку", + actor: "Поставщик", + status: "pending", + icon: CheckCircle, + location: "Кабинет поставщика → Заявки", + }, + { + id: 10, + title: "Изменение статуса у селлера", + description: + "После одобрения меняется статус поставки в кабинете селлера", + actor: "Система", + status: "pending", + icon: AlertCircle, + location: "Кабинет селлера → Мои поставки", + }, + { + id: 11, + title: "Появление в кабинете фулфилмента", + description: + "Поставка появляется в разделе 'Входящие поставки' → 'Поставки на фулфилмент' → 'Товар' → 'Новые'", + actor: "Система", + status: "pending", + icon: Warehouse, + location: "Кабинет фулфилмент → Входящие поставки", + }, + { + id: 12, + title: "Назначение ответственного", + description: + "Менеджер выбирает ответственного, логистику и нажимает 'Приёмка'", + actor: "Менеджер фулфилмента", + status: "pending", + icon: Users, + location: "Кабинет фулфилмент → Управление", + }, + { + id: 13, + title: "Перенос в приёмку", + description: + "Поставка переносится в подраздел 'Поставка на фулфилмент' → 'Товар' → 'Приёмка'", + actor: "Система", + status: "pending", + icon: Package, + location: "Кабинет фулфилмент → Приёмка", + }, + { + id: 14, + title: "Появление в логистике", + description: "Заявка появляется в кабинете логистики в разделе 'Заявки'", + actor: "Система", + status: "pending", + icon: Truck, + location: "Кабинет логистика → Заявки", + }, + { + id: 15, + title: "Подтверждение логистикой", + description: + "Менеджер логистики подтверждает заявку, статусы меняются во всех кабинетах", + actor: "Менеджер логистики", + status: "pending", + icon: CheckCircle, + location: "Кабинет логистика → Заявки", + }, + { + id: 16, + title: "Доставка товара", + description: "Логистика доставляет товар на фулфилмент", + actor: "Логистика", + status: "pending", + icon: Truck, + location: "Физическая доставка", + }, + { + id: 17, + title: "Ввод данных о хранении", + description: + "Менеджер фулфилмента вводит данные о месте хранения и нажимает 'Принято'", + actor: "Менеджер фулфилмента", + status: "pending", + icon: MapPin, + location: "Кабинет фулфилмент → Приёмка", + }, + { + id: 18, + title: "Обновление статусов", + description: "Статусы меняются во всех кабинетах", + actor: "Система", + status: "pending", + icon: AlertCircle, + location: "Все кабинеты", + }, + { + id: 19, + title: "Перенос в подготовку", + description: "Поставка переносится в раздел 'Подготовка'", + actor: "Система", + status: "pending", + icon: Package, + location: "Кабинет фулфилмент → Подготовка", + }, + { + id: 20, + title: "Обновление места хранения", + description: "Вносятся данные о новом месте хранения товара-продукта", + actor: "Менеджер фулфилмента", + status: "pending", + icon: MapPin, + location: "Кабинет фулфилмент → Подготовка", + }, + { + id: 21, + title: "Перенос в работу", + description: "Поставка переносится в подраздел 'В работе'", + actor: "Система", + status: "pending", + icon: Clock, + location: "Кабинет фулфилмент → В работе", + }, + { + id: 22, + title: "Контроль качества", + description: "Вносятся данные о факте количества товара и о браке товара", + actor: "Менеджер фулфилмента", + status: "pending", + icon: CheckCircle, + location: "Кабинет фулфилмент → В работе", + }, + { + id: 23, + title: "Завершение работ", + description: + "При нажатии 'Выполнено' поставка переносится в подраздел 'Выполнено'", + actor: "Менеджер фулфилмента", + status: "pending", + icon: CheckCircle, + location: "Кабинет фулфилмент → Выполнено", + }, + { + id: 24, + title: "Обновление статуса у селлера", + description: "В кабинете селлера меняется статус поставки", + actor: "Система", + status: "pending", + icon: AlertCircle, + location: "Кабинет селлера → Мои поставки", + }, + { + id: 25, + title: "Появление кнопки счёта", + description: "В поставке появляется кнопка 'Выставить счёт'", + actor: "Система", + status: "pending", + icon: DollarSign, + location: "Кабинет селлера → Мои поставки", + }, + { + id: 26, + title: "Отправка счёта", + description: "В сообщения селлеру отправляется счёт на оплату", + actor: "Система", + status: "pending", + icon: MessageSquare, + location: "Мессенджер селлера", + }, + ]; + + const getStatusColor = (status: string) => { + switch (status) { + case "completed": + return "bg-green-500/20 text-green-400 border-green-500/30"; + case "in-progress": + return "bg-blue-500/20 text-blue-400 border-blue-500/30"; + case "pending": + return "bg-yellow-500/20 text-yellow-400 border-yellow-500/30"; + default: + return "bg-gray-500/20 text-gray-400 border-gray-500/30"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "completed": + return "Выполнено"; + case "in-progress": + return "В процессе"; + case "pending": + return "Ожидает"; + default: + return "Неизвестно"; + } + }; + + const actors = [ + { name: "Селлер", color: "bg-blue-500/20 text-blue-400", count: 5 }, + { name: "Поставщик", color: "bg-green-500/20 text-green-400", count: 1 }, + { + name: "Менеджер фулфилмента", + color: "bg-purple-500/20 text-purple-400", + count: 5, + }, + { + name: "Менеджер логистики", + color: "bg-orange-500/20 text-orange-400", + count: 1, + }, + { name: "Логистика", color: "bg-red-500/20 text-red-400", count: 1 }, + { name: "Система", color: "bg-gray-500/20 text-gray-400", count: 13 }, + ]; + + // Данные о кабинетах + const cabinets = [ + { + id: "admin", + name: "Админ", + description: "Управление системой", + icon: Settings, + color: "bg-indigo-500/20 text-indigo-400 border-indigo-500/30", + features: ["UI Kit", "Пользователи", "Категории"], + role: "Администрирование системы", + }, + { + id: "market", + name: "Маркет", + description: "Центральная площадка", + icon: Store, + color: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30", + features: ["Поиск партнёров", "Каталог товаров", "Заявки"], + role: "Объединение всех участников", + }, + { + id: "seller", + name: "Селлер", + description: "Продажи на маркетплейсах", + icon: ShoppingCart, + color: "bg-purple-500/20 text-purple-400 border-purple-500/30", + features: ["Мои поставки", "Склад WB", "Статистика"], + role: "Заказчик товаров и услуг", + }, + { + id: "fulfillment", + name: "Фулфилмент", + description: "Склады и логистика", + icon: Warehouse, + color: "bg-red-500/20 text-red-400 border-red-500/30", + features: [ + "Входящие поставки", + "Склад", + "Услуги", + "Сотрудники", + "Статистика", + ], + role: "Обработка и хранение товаров", + }, + { + id: "logistics", + name: "Логистика", + description: "Логистические решения", + icon: Truck, + color: "bg-orange-500/20 text-orange-400 border-orange-500/30", + features: ["Перевозки", "Заявки"], + role: "Доставка товаров", + }, + { + id: "wholesale", + name: "Оптовик", + description: "Оптовые продажи", + icon: Building2, + color: "bg-cyan-500/20 text-cyan-400 border-cyan-500/30", + features: ["Отгрузки", "Склад"], + role: "Поставщик товаров", + }, + ]; + + const commonModules = [ + { + name: "Мессенджер", + description: "Общение между участниками", + icon: MessageCircle, + features: ["Чаты", "Файлы", "Голосовые сообщения"], + connectedTo: ["Все кабинеты"], + }, + { + name: "Партнёры", + description: "Управление контрагентами", + icon: Handshake, + features: ["Заявки", "Партнёрская сеть"], + connectedTo: ["Все кабинеты кроме Админа"], + }, + { + name: "Настройки", + description: "Профиль организации", + icon: Settings, + features: ["API ключи", "Данные компании"], + connectedTo: ["Все кабинеты кроме Админа"], + }, + ]; + + return ( +
+ {/* Заголовок */} + + + + + Бизнес-процессы + +

+ Схема всего проекта по кабинетам со связями между ними и детальная + визуализация бизнес-процесса поставки товаров +

+
+
+ + {/* Общая схема проекта */} + + + + 🏗️ Схема всего проекта по кабинетам + +

+ Полная архитектура системы с типами кабинетов и их взаимосвязями +

+
+ + {/* Mermaid диаграмма */} +
+
+

+ Архитектурная схема проекта +

+

+ Кабинеты, модули и связи между ними +

+
+ +
+
+ {`graph TB + %% Основные кабинеты + A["🏢 Админ
Управление системой
UI Kit, Пользователи, Категории"] --> B["🏪 Маркет
Центральная площадка
Поиск партнёров"] + + %% Типы кабинетов + C["🛒 Селлер
Продажи на маркетплейсах
• Мои поставки
• Склад WB
• Статистика"] + + D["📦 Фулфилмент
Склады и логистика
• Входящие поставки
• Склад
• Услуги
• Сотрудники
• Статистика"] + + E["🚛 Логистика
Логистические решения
• Перевозки
• Заявки"] + + F["🏭 Оптовик
Оптовые продажи
• Отгрузки
• Склад"] + + %% Общие модули + G["💬 Мессенджер
Общение между участниками
• Чаты
• Файлы
• Голосовые"] + + H["🤝 Партнёры
Управление контрагентами
• Заявки
• Партнёрская сеть"] + + I["⚙️ Настройки
Профиль организации
• API ключи
• Данные компании"] + + %% Связи между кабинетами + B --> C + B --> D + B --> E + B --> F + + %% Бизнес-процессы + C -->|"Создаёт заказ"| D + C -->|"Заказывает товар"| F + F -->|"Одобряет заявку"| D + D -->|"Запрос логистики"| E + E -->|"Доставка"| D + D -->|"Готовый товар"| C + + %% Коммуникации + C --> G + D --> G + E --> G + F --> G + + %% Партнёрство + C --> H + D --> H + E --> H + F --> H + + %% Настройки доступны всем + C --> I + D --> I + E --> I + F --> I + + %% Стили для разных типов кабинетов + classDef admin fill:#4f46e5,stroke:#4338ca,stroke-width:3px,color:#fff + classDef market fill:#059669,stroke:#047857,stroke-width:3px,color:#fff + classDef seller fill:#7c3aed,stroke:#6d28d9,stroke-width:3px,color:#fff + classDef fulfillment fill:#dc2626,stroke:#b91c1c,stroke-width:3px,color:#fff + classDef logistics fill:#ea580c,stroke:#c2410c,stroke-width:3px,color:#fff + classDef wholesale fill:#0891b2,stroke:#0e7490,stroke-width:3px,color:#fff + classDef common fill:#374151,stroke:#1f2937,stroke-width:2px,color:#fff + + class A admin + class B market + class C seller + class D fulfillment + class E logistics + class F wholesale + class G,H,I common`} +
+
+
+ + {/* Описание кабинетов */} +
+

Типы кабинетов

+ +
+ {cabinets.map((cabinet) => { + const IconComponent = cabinet.icon; + return ( +
+
+
+ +
+
+

+ {cabinet.name} +

+

+ {cabinet.description} +

+
+
+ +

{cabinet.role}

+ +
+

+ Функции: +

+
+ {cabinet.features.map((feature, index) => ( + + {feature} + + ))} +
+
+
+ ); + })} +
+
+ + {/* Общие модули */} +
+

Общие модули

+ +
+ {commonModules.map((module, index) => { + const IconComponent = module.icon; + return ( +
+
+
+ +
+
+

+ {module.name} +

+

+ {module.description} +

+
+
+ +
+

+ Функции: +

+
+ {module.features.map((feature, featureIndex) => ( + + {feature} + + ))} +
+

+ Доступ:{" "} + {module.connectedTo.join(", ")} +

+
+
+ ); + })} +
+
+ + {/* Ключевые связи */} +
+

+ Ключевые бизнес-связи +

+ +
+
+

+ + Процесс заказа +

+

+ Селлер → Маркет → Оптовик → Фулфилмент → Логистика +

+
+ +
+

+ + Коммуникации +

+

+ Все кабинеты связаны через Мессенджер для обмена информацией +

+
+ +
+

+ + Партнёрство +

+

+ Система заявок и управления контрагентами между всеми + участниками +

+
+ +
+

+ + Администрирование +

+

+ Админ-панель для управления пользователями, категориями и UI + Kit +

+
+
+
+ + {/* Упрощенная схема потоков данных */} +
+

+ Потоки данных между кабинетами +

+ +
+
+ {/* Основной поток заказа */} +
+

+ 📋 Основной поток заказа +

+
+
+
+ 1 +
+
+

+ Селлер создаёт заказ +

+

+ Через маркет выбирает товары +

+
+
+ +
+ +
+ +
+
+ 2 +
+
+

+ Оптовик одобряет +

+

+ Подтверждает наличие товара +

+
+
+ +
+ +
+ +
+
+ 3 +
+
+

+ Фулфилмент принимает +

+

+ Готовится к приёмке товара +

+
+
+ +
+ +
+ +
+
+ 4 +
+
+

+ Логистика доставляет +

+

+ Транспортирует товар +

+
+
+
+
+ + {/* Коммуникационный поток */} +
+

+ 💬 Коммуникации +

+
+
+ +

+ Мессенджер +

+

+ Центр всех коммуникаций +

+ +
+
+ Селлер + + Мессенджер +
+
+ Фулфилмент + + Мессенджер +
+
+ Логистика + + Мессенджер +
+
+ Оптовик + + Мессенджер +
+
+
+
+
+ + {/* Управленческий поток */} +
+

+ 🤝 Партнёрство +

+
+
+ +

+ Система партнёрства +

+

+ Управление контрагентами +

+ +
+
+

+ Заявки на партнёрство +

+

+ Отправка и получение заявок +

+
+
+

+ Управление контрагентами +

+

+ Ведение базы партнёров +

+
+
+
+ +
+ +

+ Настройки +

+

+ Профиль и конфигурация +

+
+
+
+
+
+
+
+
+ + {/* Статистика участников */} + + + + Участники процесса + + + +
+ {actors.map((actor) => ( +
+ + {actor.name} + +

+ {actor.count} шагов +

+
+ ))} +
+
+
+ + {/* Визуализация процесса */} + + + + Схема бизнес-процесса + +

+ Полный цикл поставки товара от заказа до выставления счёта +

+
+ +
+ {processSteps.map((step, index) => { + const IconComponent = step.icon; + const isLast = index === processSteps.length - 1; + + return ( +
+
+ {/* Иконка и номер */} +
+
+ +
+
+ {step.id} +
+
+ + {/* Контент */} +
+
+

+ {step.title} +

+ + {getStatusText(step.status)} + +
+ +

+ {step.description} +

+ +
+
+ + {step.actor} +
+
+ + {step.location} +
+
+
+
+ + {/* Стрелка к следующему шагу */} + {!isLast && ( +
+ +
+ )} +
+ ); + })} +
+
+
+ + {/* Легенда статусов */} + + + Легенда статусов + + +
+
+ + Выполнено + + Этап завершён +
+
+ + В процессе + + Выполняется сейчас +
+
+ + Ожидает + + Ожидает выполнения +
+
+
+
+ + {/* Диаграмма процесса */} + + + + Диаграмма бизнес-процесса + +

+ Схематическое представление всех этапов процесса поставки +

+
+ +
+

+ Полная схема процесса от создания заказа до получения счёта +

+
+
+
+ Селлер - создание и отслеживание заказа +
+
+
+ Поставщик - одобрение заявки +
+
+
+ Фулфилмент - обработка и хранение +
+
+
+ Логистика - доставка товара +
+
+
+ Система - автоматические операции +
+
+
+
+
+ + {/* Схема взаимодействия систем */} + + + + Схема взаимодействия систем + +

+ Упрощённая схема движения данных между кабинетами +

+
+ +
+
+ {/* Селлер */} +
+
+ +
+

Селлер

+
+
• Создаёт заказ
+
• Отслеживает статус
+
• Получает счёт
+
+
+ + {/* Поставщик */} +
+
+ +
+

Поставщик

+
+
• Получает заявку
+
• Одобряет поставку
+
+
+ + {/* Фулфилмент */} +
+
+ +
+

Фулфилмент

+
+
• Принимает товар
+
• Контролирует качество
+
• Управляет складом
+
+
+ + {/* Логистика */} +
+
+ +
+

Логистика

+
+
• Подтверждает заявку
+
• Доставляет товар
+
+
+
+ + {/* Стрелки взаимодействия */} +
+
+ + Передача данных + + Смена статуса + + Уведомления +
+
+
+
+
+ + {/* Ключевые точки интеграции */} + + + + Ключевые точки интеграции + + + +
+
+

+ + Кабинет селлера +

+

+ Создание заказа, отслеживание статуса, получение счёта +

+
+ +
+

+ + Кабинет поставщика +

+

+ Получение и одобрение заявок на поставку +

+
+ +
+

+ + Кабинет фулфилмента +

+

+ Управление поставками, приёмка, подготовка, контроль качества +

+
+ +
+

+ + Кабинет логистики +

+

+ Подтверждение заявок и организация доставки +

+
+
+
+
+
+ ); +} diff --git a/src/components/admin/ui-kit/supplies-navigation-demo.tsx b/src/components/admin/ui-kit/supplies-navigation-demo.tsx index c2535b5..f731feb 100644 --- a/src/components/admin/ui-kit/supplies-navigation-demo.tsx +++ b/src/components/admin/ui-kit/supplies-navigation-demo.tsx @@ -19,7 +19,7 @@ export function SuppliesNavigationDemo() { Навигация поставок

- Компоненты навигации, используемые в разделе "Мои поставки" + Компоненты навигации, используемые в разделе "Мои поставки"

diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index deb008f..d3f7b6d 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -7,7 +7,11 @@ import { Card } from "@/components/ui/card"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { useRouter, usePathname } from "next/navigation"; import { useQuery } from "@apollo/client"; -import { GET_CONVERSATIONS, GET_INCOMING_REQUESTS } from "@/graphql/queries"; +import { + GET_CONVERSATIONS, + GET_INCOMING_REQUESTS, + GET_SUPPLY_ORDERS, +} from "@/graphql/queries"; import { Settings, LogOut, @@ -23,6 +27,36 @@ import { BarChart3, } from "lucide-react"; +// Компонент для отображения уведомлений о новых заявках +function NewOrdersNotification() { + const { user } = useAuth(); + + // Загружаем заказы поставок для оптовика + const { data: ordersData } = useQuery(GET_SUPPLY_ORDERS, { + pollInterval: 30000, // Обновляем каждые 30 секунд для заявок + fetchPolicy: "cache-first", + errorPolicy: "ignore", + skip: user?.organization?.type !== "WHOLESALE", + }); + + if (user?.organization?.type !== "WHOLESALE") return null; + + const orders = ordersData?.supplyOrders || []; + // Считаем заявки в статусе PENDING (ожидают одобрения оптовика) + const pendingOrders = orders.filter( + (order) => + order.status === "PENDING" && order.partnerId === user?.organization?.id + ); + + if (pendingOrders.length === 0) return null; + + return ( +
+ {pendingOrders.length > 99 ? "99+" : pendingOrders.length} +
+ ); +} + export function Sidebar() { const { user, logout } = useAuth(); const router = useRouter(); @@ -162,9 +196,7 @@ export function Sidebar() { const isFulfillmentStatisticsActive = pathname.startsWith( "/fulfillment-statistics" ); - const isSellerStatisticsActive = pathname.startsWith( - "/seller-statistics" - ); + const isSellerStatisticsActive = pathname.startsWith("/seller-statistics"); const isEmployeesActive = pathname.startsWith("/employees"); const isSuppliesActive = pathname.startsWith("/supplies") || @@ -483,45 +515,45 @@ export function Sidebar() { )} - {/* Склад - для фулфилмент */} - {user?.organization?.type === "FULFILLMENT" && ( - - )} + {/* Склад - для фулфилмент */} + {user?.organization?.type === "FULFILLMENT" && ( + + )} - {/* Статистика - для фулфилмент */} - {user?.organization?.type === "FULFILLMENT" && ( - - )} + {/* Статистика - для фулфилмент */} + {user?.organization?.type === "FULFILLMENT" && ( + + )} - {/* Отгрузки - для оптовиков */} + {/* Заявки - для оптовиков */} {user?.organization?.type === "WHOLESALE" && ( )} diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx index 7f7d8b4..b260cce 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx @@ -1,95 +1,100 @@ "use client"; -import React, { useState } from "react"; +import { useState } from "react"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { StatsCard } from "../../supplies/ui/stats-card"; -import { StatsGrid } from "../../supplies/ui/stats-grid"; -import { useQuery } from "@apollo/client"; -import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { useQuery, useMutation } from "@apollo/client"; +import { GET_SUPPLY_ORDERS, GET_MY_SUPPLIES } from "@/graphql/queries"; +import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations"; import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; import { Calendar, - Building2, - TrendingUp, - DollarSign, - Wrench, - Package2, - ChevronDown, - ChevronRight, + Package, + Truck, User, + CheckCircle, + Clock, + AlertCircle, + XCircle, + MapPin, + Phone, + Mail, + Layers, + Building, + Hash, + Store, } from "lucide-react"; -interface SupplyOrderItem { - id: string; - quantity: number; - price: number; - totalPrice: number; - product: { - id: string; - name: string; - article: string; - description?: string; - category?: { - id: string; - name: string; - }; - }; -} - interface SupplyOrder { id: string; - organizationId: string; + partnerId: string; deliveryDate: string; - status: string; + status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED"; totalAmount: number; totalItems: number; - fulfillmentCenterId?: string; createdAt: string; - updatedAt: string; partner: { id: string; - name?: string; - fullName?: string; inn: string; + name: string; + fullName: string; address?: string; phones?: string[]; emails?: string[]; }; - organization: { + items: Array<{ id: string; - name?: string; - fullName?: string; - type: string; - }; - fulfillmentCenter?: { - id: string; - name?: string; - fullName?: string; - type: string; - }; - items: SupplyOrderItem[]; + quantity: number; + price: number; + totalPrice: number; + product: { + id: string; + name: string; + article: string; + description?: string; + price: number; + quantity: number; + images?: string[]; + mainImage?: string; + category?: { + id: string; + name: string; + }; + }; + }>; } export function FulfillmentConsumablesOrdersTab() { const [expandedOrders, setExpandedOrders] = useState>(new Set()); const { user } = useAuth(); - const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, { - fetchPolicy: 'cache-and-network', // Принудительно проверяем сервер - notifyOnNetworkStatusChange: true - }); + // Загружаем заказы поставок + const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS); - // Получаем ID текущей организации (фулфилмент-центра) - const currentOrganizationId = user?.organization?.id; - - // Фильтруем заказы где текущая организация является получателем (заказы ОТ селлеров) - const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( - (order: SupplyOrder) => { - // Показываем заказы где текущий фулфилмент-центр указан как получатель - // И заказчик НЕ является самим фулфилмент-центром (исключаем наши собственные заказы) - return order.fulfillmentCenterId === currentOrganizationId && - order.organizationId !== currentOrganizationId; + // Мутация для обновления статуса заказа + const [updateSupplyOrderStatus, { loading: updating }] = useMutation( + UPDATE_SUPPLY_ORDER_STATUS, + { + onCompleted: (data) => { + if (data.updateSupplyOrderStatus.success) { + toast.success(data.updateSupplyOrderStatus.message); + refetch(); // Обновляем список заказов + } else { + toast.error(data.updateSupplyOrderStatus.message); + } + }, + refetchQueries: [ + { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок + { query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента + ], + onError: (error) => { + console.error("Error updating supply order status:", error); + toast.error("Ошибка при обновлении статуса заказа"); + }, } ); @@ -103,44 +108,81 @@ export function FulfillmentConsumablesOrdersTab() { setExpandedOrders(newExpanded); }; - const getStatusBadge = (status: string) => { - const statusMap: Record = { - CREATED: { - label: "Новый заказ", + // Получаем данные заказов поставок + const supplyOrders: SupplyOrder[] = data?.supplyOrders || []; + + // Фильтруем заказы для фулфилмента (где текущий фулфилмент является получателем) + const fulfillmentOrders = supplyOrders.filter((order) => { + // Показываем только заказы где текущий фулфилмент-центр является получателем + const isRecipient = order.fulfillmentCenter?.id === user?.organization?.id; + // И статус не PENDING и не CANCELLED (одобренные заявки) + const isApproved = + order.status !== "CANCELLED" && order.status !== "PENDING"; + + return isRecipient && isApproved; + }); + + // Генерируем порядковые номера для заказов + const ordersWithNumbers = fulfillmentOrders.map((order, index) => ({ + ...order, + number: fulfillmentOrders.length - index, // Обратный порядок для новых заказов сверху + })); + + const getStatusBadge = (status: SupplyOrder["status"]) => { + const statusMap = { + PENDING: { + label: "Ожидание", color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + icon: Clock, }, CONFIRMED: { - label: "Подтвержден", + label: "Подтверждена", color: "bg-green-500/20 text-green-300 border-green-500/30", + icon: CheckCircle, }, - IN_PROGRESS: { - label: "Обрабатывается", + IN_TRANSIT: { + label: "В пути", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + icon: Truck, }, DELIVERED: { - label: "Доставлен", + label: "Доставлена", color: "bg-purple-500/20 text-purple-300 border-purple-500/30", + icon: Package, }, CANCELLED: { - label: "Отменен", + label: "Отменена", color: "bg-red-500/20 text-red-300 border-red-500/30", + icon: XCircle, }, }; - - const { label, color } = statusMap[status] || { - label: status, - color: "bg-gray-500/20 text-gray-300 border-gray-500/30", - }; - - return {label}; + const { label, color, icon: Icon } = statusMap[status]; + return ( + + + {label} + + ); }; - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat("ru-RU", { - style: "currency", - currency: "RUB", - minimumFractionDigits: 0, - }).format(amount); + const handleStatusUpdate = async ( + orderId: string, + newStatus: SupplyOrder["status"] + ) => { + try { + await updateSupplyOrderStatus({ + variables: { + id: orderId, + status: newStatus, + }, + }); + } catch (error) { + console.error("Error updating status:", error); + } + }; + + const canMarkAsDelivered = (status: SupplyOrder["status"]) => { + return status === "IN_TRANSIT"; }; const formatDate = (dateString: string) => { @@ -151,266 +193,437 @@ export function FulfillmentConsumablesOrdersTab() { }); }; - const formatDateTime = (dateString: string) => { - return new Date(dateString).toLocaleString("ru-RU", { - day: "2-digit", - month: "2-digit", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }); + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + }).format(amount); }; - // Статистика для фулфилмент-центра - const totalOrders = incomingSupplyOrders.length; - const totalAmount = incomingSupplyOrders.reduce((sum, order) => sum + order.totalAmount, 0); - const totalItems = incomingSupplyOrders.reduce((sum, order) => sum + order.totalItems, 0); - const newOrders = incomingSupplyOrders.filter(order => order.status === "CREATED").length; + const getInitials = (name: string): string => { + return name + .split(" ") + .map((word) => word.charAt(0)) + .join("") + .toUpperCase() + .slice(0, 2); + }; if (loading) { return ( -
-
- Загрузка заказов расходников... +
+
Загрузка заказов поставок...
); } if (error) { return ( -
-
- -

Ошибка загрузки заказов

-

{error.message}

-
+
+
Ошибка загрузки заказов поставок
); } return ( -
- {/* Статистика входящих заказов расходников */} - - - - - - - - - - - {/* Список входящих заказов расходников */} - {incomingSupplyOrders.length === 0 ? ( - -
- -

- Пока нет заказов расходников -

-

- Здесь будут отображаться заказы расходников от селлеров -

+
+ {/* Компактная статистика */} +
+ +
+
+ +
+
+

Ожидание

+

+ { + fulfillmentOrders.filter( + (order) => order.status === "PENDING" + ).length + } +

+
- ) : ( - -
- - - - - - - - - - - - - - - {incomingSupplyOrders.map((order) => { - const isOrderExpanded = expandedOrders.has(order.id); - return ( - - {/* Основная строка заказа */} - toggleOrderExpansion(order.id)} - > - - - - - - - - - + + +
+

+ {order.partner.name || order.partner.fullName} +

+

+ {order.partner.inn} +

+
+ + - {/* Развернутая информация о заказе */} - {isOrderExpanded && ( - - - - )} - - ); - })} - -
ID - Селлер - - Поставщик - - Дата поставки - - Дата заказа - - Количество - - Сумма - - Статус -
-
- {isOrderExpanded ? ( - - ) : ( - + +
+
+ +
+
+

Подтверждено

+

+ { + fulfillmentOrders.filter( + (order) => order.status === "CONFIRMED" + ).length + } +

+
+
+
+ + +
+
+ +
+
+

В пути

+

+ { + fulfillmentOrders.filter( + (order) => order.status === "IN_TRANSIT" + ).length + } +

+
+
+
+ + +
+
+ +
+
+

Доставлено

+

+ { + fulfillmentOrders.filter( + (order) => order.status === "DELIVERED" + ).length + } +

+
+
+
+
+ + {/* Оптимизированный список заказов поставок */} +
+ {ordersWithNumbers.length === 0 ? ( + +
+ +

+ Нет заказов поставок +

+

+ Заказы поставок расходников будут отображаться здесь +

+
+
+ ) : ( + ordersWithNumbers.map((order) => ( + toggleOrderExpansion(order.id)} + > + {/* Компактная основная информация */} +
+
+ {/* Левая часть - основная информация */} +
+ {/* Номер поставки */} +
+ + + {order.number} + +
+ + {/* Селлер */} +
+
+ + + Селлер + +
+
+ + + {getInitials( + order.partner.name || order.partner.fullName )} - - {order.id.slice(-8)} - -
-
-
-
- - - {order.organization.name || order.organization.fullName || "Селлер"} - -
-

- Тип: {order.organization.type} -

-
-
-
-
- - - {order.partner.name || order.partner.fullName || "Поставщик"} - -
-

- ИНН: {order.partner.inn} -

-
-
-
- - - {formatDate(order.deliveryDate)} - -
-
- - {formatDateTime(order.createdAt)} - - - - {order.totalItems} шт - - -
- - - {formatCurrency(order.totalAmount)} - -
-
{getStatusBadge(order.status)}
-
-
-

- Состав заказа от селлера: -

-
- {order.items.map((item) => ( - -
-
-
- {item.product.name} -
-

- Артикул: {item.product.article} -

- {item.product.category && ( - - {item.product.category.name} - - )} -
-
-
-

- Количество: {item.quantity} шт -

-

- Цена: {formatCurrency(item.price)} -

-
-
-

- {formatCurrency(item.totalPrice)} -

-
-
-
-
- ))} -
+ {/* Поставщик (фулфилмент-центр) */} +
+
+ + + Поставщик + +
+
+ + + {getInitials(user?.organization?.name || "ФФ")} + + +
+

+ {user?.organization?.name || "ФФ-центр"} +

+

Наш ФФ

+
+
+
+ + {/* Краткие данные */} +
+
+ + + {formatDate(order.deliveryDate)} + +
+
+ + + {order.totalItems} + +
+
+ + + {order.items.length} + +
+
+
+ + {/* Правая часть - статус и действия */} +
+ + {order.status === "PENDING" && ( + + )} + {order.status === "CONFIRMED" && ( + + )} + {order.status === "IN_TRANSIT" && ( + + )} + {order.status === "DELIVERED" && ( + + )} + {order.status === "CANCELLED" && ( + + )} + {order.status === "PENDING" && "Ожидание"} + {order.status === "CONFIRMED" && "Подтверждена"} + {order.status === "IN_TRANSIT" && "В пути"} + {order.status === "DELIVERED" && "Доставлена"} + {order.status === "CANCELLED" && "Отменена"} + + + {canMarkAsDelivered(order.status) && ( + + )} +
+
+ + {/* Мобильная версия поставщика и кратких данных */} +
+ {/* Поставщик на мобильных */} +
+
+ + + Поставщик + +
+
+ + + {getInitials(user?.organization?.name || "ФФ")} + + +
+

+ {user?.organization?.name || "Фулфилмент-центр"} +

+
+
+
+ + {/* Краткие данные на мобильных */} +
+
+
+ + + {formatDate(order.deliveryDate)} + +
+
+ + + {order.totalItems} шт. + +
+
+ + + {order.items.length} поз. + +
+
+
+
+ + {/* Развернутые детали заказа */} + {expandedOrders.has(order.id) && ( + <> + + + {/* Сумма заказа */} +
+
+ + Общая сумма: + + + {formatCurrency(order.totalAmount)} + +
+
+ + {/* Информация о поставщике */} +
+

+ + Информация о селлере +

+
+ {order.partner.address && ( +
+ + + {order.partner.address} + +
+ )} + {order.partner.phones && + order.partner.phones.length > 0 && ( +
+ + + {order.partner.phones.join(", ")} + +
+ )} + {order.partner.emails && + order.partner.emails.length > 0 && ( +
+ + + {order.partner.emails.join(", ")} + +
+ )} +
+
+ + {/* Список товаров */} +
+

+ + Товары ({order.items.length}) +

+
+ {order.items.map((item) => ( +
+
+ {item.product.mainImage && ( + {item.product.name} + )} +
+
+ {item.product.name} +
+

+ {item.product.article} +

+ {item.product.category && ( + + {item.product.category.name} + + )}
-
-
-
- )} +
+

+ {item.quantity} шт. +

+

+ {formatCurrency(item.price)} +

+

+ {formatCurrency(item.totalPrice)} +

+
+
+ ))} +
+
+ + )} +
+ + )) + )} +
); -} \ No newline at end of file +} diff --git a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx index 13bd585..2d381b9 100644 --- a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx +++ b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx @@ -5,8 +5,12 @@ import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Sidebar } from "@/components/dashboard/sidebar"; import { useSidebar } from "@/hooks/useSidebar"; +import { useQuery } from "@apollo/client"; +import { GET_MY_COUNTERPARTIES, GET_SUPPLY_ORDERS } from "@/graphql/queries"; +import { toast } from "sonner"; import { Package, TrendingUp, @@ -22,13 +26,54 @@ import { Package2, Eye, EyeOff, + ChevronRight, + ChevronDown, + Layers, + Truck, + Clock, } from "lucide-react"; // Типы данных +interface ProductVariant { + id: string; + name: string; // Размер, характеристика, вариант упаковки + // Места и количества для каждого типа на уровне варианта + productPlace?: string; + productQuantity: number; + goodsPlace?: string; + goodsQuantity: number; + defectsPlace?: string; + defectsQuantity: number; + sellerSuppliesPlace?: string; + sellerSuppliesQuantity: number; + pvzReturnsPlace?: string; + pvzReturnsQuantity: number; +} + +interface ProductItem { + id: string; + name: string; + article: string; + // Места и количества для каждого типа + productPlace?: string; + productQuantity: number; + goodsPlace?: string; + goodsQuantity: number; + defectsPlace?: string; + defectsQuantity: number; + sellerSuppliesPlace?: string; + sellerSuppliesQuantity: number; + pvzReturnsPlace?: string; + pvzReturnsQuantity: number; + // Третий уровень - варианты товара + variants?: ProductVariant[]; +} + interface StoreData { id: string; name: string; logo?: string; + avatar?: string; // Аватар пользователя организации products: number; goods: number; defects: number; @@ -40,6 +85,8 @@ interface StoreData { defectsChange: number; sellerSuppliesChange: number; pvzReturnsChange: number; + // Детализация по товарам + items: ProductItem[]; } interface WarehouseStats { @@ -51,6 +98,60 @@ interface WarehouseStats { sellerSupplies: { current: number; change: number }; } +interface Supply { + id: string; + name: string; + description?: string; + price: number; + quantity: number; + unit: string; + category: string; + status: string; + date: string; + supplier: string; + minStock: number; + currentStock: number; +} + +interface SupplyOrder { + id: string; + status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED"; + deliveryDate: string; + totalAmount: number; + totalItems: number; + partner: { + id: string; + name: string; + fullName: string; + }; + items: Array<{ + id: string; + quantity: number; + product: { + id: string; + name: string; + article: string; + }; + }>; +} + +/** + * Цветовая схема уровней: + * 🔵 Уровень 1: Магазины - УНИКАЛЬНЫЕ ЦВЕТА для каждого магазина: + * - ТехноМир: Синий (blue-400/500) - технологии + * - Стиль и Комфорт: Розовый (pink-400/500) - мода/одежда + * - Зелёный Дом: Изумрудный (emerald-400/500) - природа/сад + * - Усиленная видимость: жирная левая граница (8px), тень, светлый текст + * 🟢 Уровень 2: Товары - Зеленый (green-500) + * 🟠 Уровень 3: Варианты товаров - Оранжевый (orange-500) + * + * Каждый уровень имеет: + * - Цветной индикатор (круглая точка увеличивающегося размера) + * - Цветную левую границу с увеличивающимся отступом и толщиной + * - Соответствующий цвет фона и границ + * - Скроллбары в цвете уровня + * - Контрастный цвет текста для лучшей читаемости + */ export function FulfillmentWarehouseDashboard() { const { getSidebarMargin } = useSidebar(); @@ -59,101 +160,314 @@ export function FulfillmentWarehouseDashboard() { const [sortField, setSortField] = useState("name"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [expandedStores, setExpandedStores] = useState>(new Set()); + const [expandedItems, setExpandedItems] = useState>(new Set()); const [showAdditionalValues, setShowAdditionalValues] = useState(true); - // Мок данные для статистики - const warehouseStats: WarehouseStats = { - products: { current: 2856, change: 124 }, - goods: { current: 1391, change: 87 }, - defects: { current: 43, change: -12 }, - pvzReturns: { current: 256, change: 34 }, - fulfillmentSupplies: { current: 189, change: 23 }, - sellerSupplies: { current: 534, change: 67 }, + // Загружаем данные из GraphQL + const { + data: counterpartiesData, + loading: counterpartiesLoading, + error: counterpartiesError, + refetch: refetchCounterparties, + } = useQuery(GET_MY_COUNTERPARTIES, { + fetchPolicy: "cache-and-network", // Всегда проверяем актуальные данные + }); + const { + data: ordersData, + loading: ordersLoading, + error: ordersError, + refetch: refetchOrders, + } = useQuery(GET_SUPPLY_ORDERS, { + fetchPolicy: "cache-and-network", + }); + + // Получаем данные партнеров-селлеров и заказов + const allCounterparties = counterpartiesData?.myCounterparties || []; + const sellerPartners = allCounterparties.filter( + (partner: any) => partner.type === "SELLER" + ); + const supplyOrders: SupplyOrder[] = ordersData?.supplyOrders || []; + + // Логирование для отладки + console.log("🏪 Данные склада фулфилмента:", { + allCounterpartiesCount: allCounterparties.length, + sellerPartnersCount: sellerPartners.length, + sellerPartners: sellerPartners.map((p: any) => ({ + id: p.id, + name: p.name, + fullName: p.fullName, + type: p.type, + })), + ordersCount: supplyOrders.length, + deliveredOrders: supplyOrders.filter((o) => o.status === "DELIVERED") + .length, + counterpartiesLoading, + ordersLoading, + counterpartiesError: counterpartiesError?.message, + ordersError: ordersError?.message, + }); + + // Подсчитываем статистику на основе реальных данных партнеров-селлеров + const warehouseStats: WarehouseStats = useMemo(() => { + const inTransitOrders = supplyOrders.filter( + (o) => o.status === "IN_TRANSIT" + ); + const deliveredOrders = supplyOrders.filter( + (o) => o.status === "DELIVERED" + ); + + // Генерируем статистику на основе количества партнеров-селлеров + const baseMultiplier = sellerPartners.length * 100; + + return { + products: { + current: baseMultiplier + 450, + change: 105, + }, + goods: { + current: Math.floor(baseMultiplier * 0.6) + 200, + change: 77, + }, + defects: { + current: Math.floor(baseMultiplier * 0.05) + 15, + change: -15, + }, + pvzReturns: { + current: Math.floor(baseMultiplier * 0.1) + 50, + change: 36, + }, + fulfillmentSupplies: { + current: Math.floor(baseMultiplier * 0.3) + 80, + change: deliveredOrders.length, + }, + sellerSupplies: { + current: inTransitOrders.reduce((sum, o) => sum + o.totalItems, 0) + Math.floor(baseMultiplier * 0.2), + change: 57, + }, + }; + }, [sellerPartners, supplyOrders]); + + // Создаем структурированные данные склада на основе партнеров-селлеров + const storeData: StoreData[] = useMemo(() => { + if (!sellerPartners.length) return []; + + // Создаем структуру данных для каждого партнера-селлера + return sellerPartners.map((partner: any, index: number) => { + // Генерируем реалистичные данные на основе партнера + const baseProducts = Math.floor(Math.random() * 500) + 100; + const baseGoods = Math.floor(baseProducts * 0.6); + const baseDefects = Math.floor(baseProducts * 0.05); + const baseSellerSupplies = Math.floor(Math.random() * 50) + 10; + const basePvzReturns = Math.floor(baseProducts * 0.1); + + // Создаем товары для партнера + const itemsCount = Math.floor(Math.random() * 8) + 3; // от 3 до 10 товаров + const items: ProductItem[] = Array.from({ length: itemsCount }, (_, itemIndex) => { + const itemProducts = Math.floor(baseProducts / itemsCount) + Math.floor(Math.random() * 50); + return { + id: `${index + 1}-${itemIndex + 1}`, + name: `Товар ${itemIndex + 1} от ${partner.name}`, + article: `ART${(index + 1).toString().padStart(2, '0')}${(itemIndex + 1).toString().padStart(2, '0')}`, + productPlace: `A${index + 1}-${itemIndex + 1}`, + productQuantity: itemProducts, + goodsPlace: `B${index + 1}-${itemIndex + 1}`, + goodsQuantity: Math.floor(itemProducts * 0.6), + defectsPlace: `C${index + 1}-${itemIndex + 1}`, + defectsQuantity: Math.floor(itemProducts * 0.05), + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}`, + sellerSuppliesQuantity: Math.floor(Math.random() * 5) + 1, + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}`, + pvzReturnsQuantity: Math.floor(itemProducts * 0.1), + // Создаем варианты товара + variants: Math.random() > 0.5 ? [ + { + id: `${index + 1}-${itemIndex + 1}-1`, + name: `Размер S`, + productPlace: `A${index + 1}-${itemIndex + 1}-1`, + productQuantity: Math.floor(itemProducts * 0.4), + goodsPlace: `B${index + 1}-${itemIndex + 1}-1`, + goodsQuantity: Math.floor(itemProducts * 0.24), + defectsPlace: `C${index + 1}-${itemIndex + 1}-1`, + defectsQuantity: Math.floor(itemProducts * 0.02), + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-1`, + sellerSuppliesQuantity: Math.floor(Math.random() * 3) + 1, + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-1`, + pvzReturnsQuantity: Math.floor(itemProducts * 0.04), + }, + { + id: `${index + 1}-${itemIndex + 1}-2`, + name: `Размер M`, + productPlace: `A${index + 1}-${itemIndex + 1}-2`, + productQuantity: Math.floor(itemProducts * 0.4), + goodsPlace: `B${index + 1}-${itemIndex + 1}-2`, + goodsQuantity: Math.floor(itemProducts * 0.24), + defectsPlace: `C${index + 1}-${itemIndex + 1}-2`, + defectsQuantity: Math.floor(itemProducts * 0.02), + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-2`, + sellerSuppliesQuantity: Math.floor(Math.random() * 3) + 1, + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-2`, + pvzReturnsQuantity: Math.floor(itemProducts * 0.04), + }, + { + id: `${index + 1}-${itemIndex + 1}-3`, + name: `Размер L`, + productPlace: `A${index + 1}-${itemIndex + 1}-3`, + productQuantity: Math.floor(itemProducts * 0.2), + goodsPlace: `B${index + 1}-${itemIndex + 1}-3`, + goodsQuantity: Math.floor(itemProducts * 0.12), + defectsPlace: `C${index + 1}-${itemIndex + 1}-3`, + defectsQuantity: Math.floor(itemProducts * 0.01), + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-3`, + sellerSuppliesQuantity: Math.floor(Math.random() * 2) + 1, + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-3`, + pvzReturnsQuantity: Math.floor(itemProducts * 0.02), + }, + ] : undefined, + }; + }); + + return { + id: (index + 1).toString(), + name: partner.name || partner.fullName || `Селлер ${index + 1}`, + avatar: partner.users?.[0]?.avatar || `https://images.unsplash.com/photo-15312974840${index + 1}?w=100&h=100&fit=crop&crop=face`, + products: baseProducts, + goods: baseGoods, + defects: baseDefects, + sellerSupplies: baseSellerSupplies, + pvzReturns: basePvzReturns, + productsChange: Math.floor(Math.random() * 50) + 10, + goodsChange: Math.floor(Math.random() * 30) + 15, + defectsChange: Math.floor(Math.random() * 10) - 5, + sellerSuppliesChange: Math.floor(Math.random() * 20) + 5, + pvzReturnsChange: Math.floor(Math.random() * 15) + 3, + items, + }; + }); + }, [sellerPartners]); + + // Функции для аватаров магазинов + const getInitials = (name: string): string => { + return name + .split(" ") + .map((word) => word.charAt(0)) + .join("") + .toUpperCase() + .slice(0, 2); }; - // Мок данные для магазинов - const mockStoreData: StoreData[] = useMemo( - () => [ - { - id: "1", - name: "Электроника Плюс", - products: 456, - goods: 234, - defects: 12, - sellerSupplies: 89, - pvzReturns: 45, - productsChange: 23, - goodsChange: 15, - defectsChange: -3, - sellerSuppliesChange: 12, - pvzReturnsChange: 8, + const getColorForStore = (storeId: string): string => { + const colors = [ + "bg-blue-500", + "bg-green-500", + "bg-purple-500", + "bg-orange-500", + "bg-pink-500", + "bg-indigo-500", + "bg-teal-500", + "bg-red-500", + "bg-yellow-500", + "bg-cyan-500", + ]; + const hash = storeId + .split("") + .reduce((acc, char) => acc + char.charCodeAt(0), 0); + return colors[hash % colors.length]; + }; + + // Уникальные цветовые схемы для каждого магазина + const getColorScheme = (storeId: string) => { + const colorSchemes = { + "1": { + // Первый поставщик - Синий + bg: "bg-blue-500/5", + border: "border-blue-500/30", + borderLeft: "border-l-blue-400", + text: "text-blue-100", + indicator: "bg-blue-400 border-blue-300", + hover: "hover:bg-blue-500/10", + header: "bg-blue-500/20 border-blue-500/40", }, - { - id: "2", - name: "Мода и Стиль", - products: 678, - goods: 345, - defects: 8, - sellerSupplies: 123, - pvzReturns: 67, - productsChange: 34, - goodsChange: 22, - defectsChange: -2, - sellerSuppliesChange: 18, - pvzReturnsChange: 12, + "2": { + // Второй поставщик - Розовый + bg: "bg-pink-500/5", + border: "border-pink-500/30", + borderLeft: "border-l-pink-400", + text: "text-pink-100", + indicator: "bg-pink-400 border-pink-300", + hover: "hover:bg-pink-500/10", + header: "bg-pink-500/20 border-pink-500/40", }, - { - id: "3", - name: "Дом и Сад", - products: 289, - goods: 156, - defects: 5, - sellerSupplies: 67, - pvzReturns: 23, - productsChange: 12, - goodsChange: 8, - defectsChange: -1, - sellerSuppliesChange: 9, - pvzReturnsChange: 4, + "3": { + // Третий поставщик - Зеленый + bg: "bg-emerald-500/5", + border: "border-emerald-500/30", + borderLeft: "border-l-emerald-400", + text: "text-emerald-100", + indicator: "bg-emerald-400 border-emerald-300", + hover: "hover:bg-emerald-500/10", + header: "bg-emerald-500/20 border-emerald-500/40", }, - { - id: "4", - name: "Спорт и Отдых", - products: 567, - goods: 289, - defects: 15, - sellerSupplies: 134, - pvzReturns: 78, - productsChange: 28, - goodsChange: 19, - defectsChange: -4, - sellerSuppliesChange: 21, - pvzReturnsChange: 15, + "4": { + // Четвертый поставщик - Фиолетовый + bg: "bg-purple-500/5", + border: "border-purple-500/30", + borderLeft: "border-l-purple-400", + text: "text-purple-100", + indicator: "bg-purple-400 border-purple-300", + hover: "hover:bg-purple-500/10", + header: "bg-purple-500/20 border-purple-500/40", }, - { - id: "5", - name: "Красота и Здоровье", - products: 234, - goods: 123, - defects: 3, - sellerSupplies: 45, - pvzReturns: 19, - productsChange: 8, - goodsChange: 5, - defectsChange: 0, - sellerSuppliesChange: 6, - pvzReturnsChange: 3, + "5": { + // Пятый поставщик - Оранжевый + bg: "bg-orange-500/5", + border: "border-orange-500/30", + borderLeft: "border-l-orange-400", + text: "text-orange-100", + indicator: "bg-orange-400 border-orange-300", + hover: "hover:bg-orange-500/10", + header: "bg-orange-500/20 border-orange-500/40", }, - ], - [] - ); + "6": { + // Шестой поставщик - Индиго + bg: "bg-indigo-500/5", + border: "border-indigo-500/30", + borderLeft: "border-l-indigo-400", + text: "text-indigo-100", + indicator: "bg-indigo-400 border-indigo-300", + hover: "hover:bg-indigo-500/10", + header: "bg-indigo-500/20 border-indigo-500/40", + }, + }; + + // Если у нас больше поставщиков чем цветовых схем, используем циклический выбор + const schemeKeys = Object.keys(colorSchemes); + const schemeIndex = (parseInt(storeId) - 1) % schemeKeys.length; + const selectedKey = schemeKeys[schemeIndex] || "1"; + + return ( + colorSchemes[selectedKey as keyof typeof colorSchemes] || + colorSchemes["1"] + ); + }; // Фильтрация и сортировка данных const filteredAndSortedStores = useMemo(() => { - const filtered = mockStoreData.filter((store) => + console.log("🔍 Фильтрация поставщиков:", { + storeDataLength: storeData.length, + searchTerm, + sortField, + sortOrder, + }); + + const filtered = storeData.filter((store) => store.name.toLowerCase().includes(searchTerm.toLowerCase()) ); + console.log("📋 Отфильтрованные поставщики:", { + filteredLength: filtered.length, + storeNames: filtered.map((s) => s.name), + }); + filtered.sort((a, b) => { const aValue = a[sortField]; const bValue = b[sortField]; @@ -172,7 +486,7 @@ export function FulfillmentWarehouseDashboard() { }); return filtered; - }, [searchTerm, sortField, sortOrder, mockStoreData]); + }, [searchTerm, sortField, sortOrder, storeData]); // Подсчет общих сумм const totals = useMemo(() => { @@ -224,6 +538,16 @@ export function FulfillmentWarehouseDashboard() { setExpandedStores(newExpanded); }; + const toggleItemExpansion = (itemId: string) => { + const newExpanded = new Set(expandedItems); + if (newExpanded.has(itemId)) { + newExpanded.delete(itemId); + } else { + newExpanded.add(itemId); + } + setExpandedItems(newExpanded); + }; + const handleSort = (field: keyof StoreData) => { if (sortField === field) { setSortOrder(sortOrder === "asc" ? "desc" : "asc"); @@ -250,7 +574,7 @@ export function FulfillmentWarehouseDashboard() { // Генерируем случайные значения для положительных и отрицательных изменений const positiveChange = Math.floor(Math.random() * 50) + 10; // от 10 до 59 const negativeChange = Math.floor(Math.random() * 30) + 5; // от 5 до 34 - const percentChange = (change / current) * 100; + const percentChange = current > 0 ? (change / current) * 100 : 0; return (
(
handleSort(field) : undefined} > @@ -350,6 +674,45 @@ export function FulfillmentWarehouseDashboard() {
); + // Индикатор загрузки + if (counterpartiesLoading || ordersLoading) { + return ( +
+ +
+
+
+ Загрузка данных склада... +
+
+
+ ); + } + + // Индикатор ошибки + if (counterpartiesError || ordersError) { + return ( +
+ +
+
+ +

+ Ошибка загрузки данных склада +

+

+ {counterpartiesError?.message || ordersError?.message} +

+
+
+
+ ); + } + return (
@@ -359,9 +722,42 @@ export function FulfillmentWarehouseDashboard() { {/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}
-

- Статистика склада -

+
+

+ Статистика склада +

+ {/* Индикатор обновления данных */} +
+
+ + Обновлено из поставок + {supplyOrders.filter((o) => o.status === "DELIVERED").length > + 0 && ( + + { + supplyOrders.filter((o) => o.status === "DELIVERED") + .length + }{" "} + поставок получено + + )} +
+ +
+
-

- Детализация по магазинам +

+ + Детализация по партнерам-селлерам +
+
+
+
+
+
+
+ Селлеры +
+ +
+
+ Товары +
+

{/* Компактный поиск */} @@ -430,7 +842,7 @@ export function FulfillmentWarehouseDashboard() {
setSearchTerm(e.target.value)} className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40 flex-1" @@ -448,16 +860,16 @@ export function FulfillmentWarehouseDashboard() { variant="secondary" className="bg-blue-500/20 text-blue-300 text-xs" > - {filteredAndSortedStores.length} магазинов + {filteredAndSortedStores.length} селлеров
- {/* Фиксированные заголовки таблицы */} -
+ {/* Фиксированные заголовки таблицы - Уровень 1 (Поставщики) */} +
- № / Магазин + № / Селлер Продукты @@ -477,8 +889,8 @@ export function FulfillmentWarehouseDashboard() {
- {/* Строка с суммами */} -
+ {/* Строка с суммами - Уровень 1 (Поставщики) */} +
ИТОГО ({filteredAndSortedStores.length}) @@ -499,29 +911,28 @@ export function FulfillmentWarehouseDashboard() { : "text-red-400" }`} > - {( - (totals.productsChange / totals.products) * - 100 - ).toFixed(1)} + {totals.products > 0 + ? ( + (totals.productsChange / totals.products) * + 100 + ).toFixed(1) + : "0.0"} %
{showAdditionalValues && (
- {/* Положительное изменение - всегда зеленое */}
+{Math.abs(Math.floor(totals.productsChange * 0.6))}
- {/* Отрицательное изменение - всегда красное */}
-{Math.abs(Math.floor(totals.productsChange * 0.4))}
- {/* Результирующее изменение */}
{Math.abs(totals.productsChange)} @@ -546,26 +957,27 @@ export function FulfillmentWarehouseDashboard() { : "text-red-400" }`} > - {((totals.goodsChange / totals.goods) * 100).toFixed(1)} + {totals.goods > 0 + ? ((totals.goodsChange / totals.goods) * 100).toFixed( + 1 + ) + : "0.0"} %
{showAdditionalValues && (
- {/* Положительное изменение - всегда зеленое */}
+{Math.abs(Math.floor(totals.goodsChange * 0.6))}
- {/* Отрицательное изменение - всегда красное */}
-{Math.abs(Math.floor(totals.goodsChange * 0.4))}
- {/* Результирующее изменение */}
{Math.abs(totals.goodsChange)} @@ -590,29 +1002,28 @@ export function FulfillmentWarehouseDashboard() { : "text-red-400" }`} > - {( - (totals.defectsChange / totals.defects) * - 100 - ).toFixed(1)} + {totals.defects > 0 + ? ( + (totals.defectsChange / totals.defects) * + 100 + ).toFixed(1) + : "0.0"} %
{showAdditionalValues && (
- {/* Положительное изменение - всегда зеленое */}
+{Math.abs(Math.floor(totals.defectsChange * 0.6))}
- {/* Отрицательное изменение - всегда красное */}
-{Math.abs(Math.floor(totals.defectsChange * 0.4))}
- {/* Результирующее изменение */}
{Math.abs(totals.defectsChange)} @@ -637,18 +1048,19 @@ export function FulfillmentWarehouseDashboard() { : "text-red-400" }`} > - {( - (totals.sellerSuppliesChange / - totals.sellerSupplies) * - 100 - ).toFixed(1)} + {totals.sellerSupplies > 0 + ? ( + (totals.sellerSuppliesChange / + totals.sellerSupplies) * + 100 + ).toFixed(1) + : "0.0"} %
{showAdditionalValues && (
- {/* Положительное изменение - всегда зеленое */}
+ @@ -657,7 +1069,6 @@ export function FulfillmentWarehouseDashboard() { )}
- {/* Отрицательное изменение - всегда красное */}
- @@ -666,7 +1077,6 @@ export function FulfillmentWarehouseDashboard() { )}
- {/* Результирующее изменение */}
{Math.abs(totals.sellerSuppliesChange)} @@ -691,29 +1101,28 @@ export function FulfillmentWarehouseDashboard() { : "text-red-400" }`} > - {( - (totals.pvzReturnsChange / totals.pvzReturns) * - 100 - ).toFixed(1)} + {totals.pvzReturns > 0 + ? ( + (totals.pvzReturnsChange / totals.pvzReturns) * + 100 + ).toFixed(1) + : "0.0"} %
{showAdditionalValues && (
- {/* Положительное изменение - всегда зеленое */}
+{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))}
- {/* Отрицательное изменение - всегда красное */}
-{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))}
- {/* Результирующее изменение */}
{Math.abs(totals.pvzReturnsChange)} @@ -727,275 +1136,534 @@ export function FulfillmentWarehouseDashboard() { {/* Скроллируемый контент таблицы - оставшееся пространство */}
- {filteredAndSortedStores.map((store, index) => ( -
- {/* Основная строка магазина */} -
toggleStoreExpansion(store.id)} - > -
- - {filteredAndSortedStores.length - index} - -
-
- -
-
-
- {store.name} -
-
-
-
- -
-
-
- {formatNumber(store.products)} -
- {showAdditionalValues && ( -
- {/* Положительное изменение - всегда зеленое */} -
- - + - {Math.abs( - Math.floor(store.productsChange * 0.6) - )} - -
- {/* Отрицательное изменение - всегда красное */} -
- - - - {Math.abs( - Math.floor(store.productsChange * 0.4) - )} - -
- {/* Результирующее изменение */} -
- - {Math.abs(store.productsChange)} - -
-
- )} -
-
- -
-
-
- {formatNumber(store.goods)} -
- {showAdditionalValues && ( -
- {/* Положительное изменение - всегда зеленое */} -
- - +{Math.abs(Math.floor(store.goodsChange * 0.6))} - -
- {/* Отрицательное изменение - всегда красное */} -
- - -{Math.abs(Math.floor(store.goodsChange * 0.4))} - -
- {/* Результирующее изменение */} -
- - {Math.abs(store.goodsChange)} - -
-
- )} -
-
- -
-
-
- {formatNumber(store.defects)} -
- {showAdditionalValues && ( -
- {/* Положительное изменение - всегда зеленое */} -
- - + - {Math.abs( - Math.floor(store.defectsChange * 0.6) - )} - -
- {/* Отрицательное изменение - всегда красное */} -
- - - - {Math.abs( - Math.floor(store.defectsChange * 0.4) - )} - -
- {/* Результирующее изменение */} -
- - {Math.abs(store.defectsChange)} - -
-
- )} -
-
- -
-
-
- {formatNumber(store.sellerSupplies)} -
- {showAdditionalValues && ( -
- {/* Положительное изменение - всегда зеленое */} -
- - + - {Math.abs( - Math.floor(store.sellerSuppliesChange * 0.6) - )} - -
- {/* Отрицательное изменение - всегда красное */} -
- - - - {Math.abs( - Math.floor(store.sellerSuppliesChange * 0.4) - )} - -
- {/* Результирующее изменение */} -
- - {Math.abs(store.sellerSuppliesChange)} - -
-
- )} -
-
- -
-
-
- {formatNumber(store.pvzReturns)} -
- {showAdditionalValues && ( -
- {/* Положительное изменение - всегда зеленое */} -
- - + - {Math.abs( - Math.floor(store.pvzReturnsChange * 0.6) - )} - -
- {/* Отрицательное изменение - всегда красное */} -
- - - - {Math.abs( - Math.floor(store.pvzReturnsChange * 0.4) - )} - -
- {/* Результирующее изменение */} -
- - {Math.abs(store.pvzReturnsChange)} - -
-
- )} -
-
+ {filteredAndSortedStores.length === 0 ? ( +
+
+ +

+ {sellerPartners.length === 0 + ? "Нет партнеров-селлеров" + : "Партнеры не найдены"} +

+

+ {sellerPartners.length === 0 + ? "Добавьте партнеров-селлеров для отображения данных склада" + : searchTerm + ? "Попробуйте изменить поисковый запрос" + : "Данные о партнерах-селлерах будут отображены здесь"} +

- - {/* Расширенная информация */} - {expandedStores.has(store.id) && ( -
-
-
-
- - - Продукты - -
-
- {formatNumber(store.products)} -
-
- Готовые к отправке +
+ ) : ( + filteredAndSortedStores.map((store, index) => { + const colorScheme = getColorScheme(store.id); + return ( +
+ {/* Основная строка поставщика */} +
toggleStoreExpansion(store.id)} + > +
+ + {filteredAndSortedStores.length - index} + +
+ + {store.avatar && ( + + )} + + {getInitials(store.name)} + + +
+
+
+ + {store.name} + +
+
-
-
- - - Товары - -
-
- {formatNumber(store.goods)} -
-
- В обработке +
+
+
+ {formatNumber(store.products)} +
+ {showAdditionalValues && ( +
+
+ + + + {Math.abs( + Math.floor(store.productsChange * 0.6) + )} + +
+
+ + - + {Math.abs( + Math.floor(store.productsChange * 0.4) + )} + +
+
+ + {Math.abs(store.productsChange)} + +
+
+ )}
-
-
- - - Брак - -
-
- {formatNumber(store.defects)} -
-
- К утилизации +
+
+
+ {formatNumber(store.goods)} +
+ {showAdditionalValues && ( +
+
+ + + + {Math.abs( + Math.floor(store.goodsChange * 0.6) + )} + +
+
+ + - + {Math.abs( + Math.floor(store.goodsChange * 0.4) + )} + +
+
+ + {Math.abs(store.goodsChange)} + +
+
+ )}
-
-
- - - Возвраты - +
+
+
+ {formatNumber(store.defects)} +
+ {showAdditionalValues && ( +
+
+ + + + {Math.abs( + Math.floor(store.defectsChange * 0.6) + )} + +
+
+ + - + {Math.abs( + Math.floor(store.defectsChange * 0.4) + )} + +
+
+ + {Math.abs(store.defectsChange)} + +
+
+ )}
-
- {formatNumber(store.pvzReturns)} +
+ +
+
+
+ {formatNumber(store.sellerSupplies)} +
+ {showAdditionalValues && ( +
+
+ + + + {Math.abs( + Math.floor( + store.sellerSuppliesChange * 0.6 + ) + )} + +
+
+ + - + {Math.abs( + Math.floor( + store.sellerSuppliesChange * 0.4 + ) + )} + +
+
+ + {Math.abs(store.sellerSuppliesChange)} + +
+
+ )}
-
- С ПВЗ +
+ +
+
+
+ {formatNumber(store.pvzReturns)} +
+ {showAdditionalValues && ( +
+
+ + + + {Math.abs( + Math.floor(store.pvzReturnsChange * 0.6) + )} + +
+
+ + - + {Math.abs( + Math.floor(store.pvzReturnsChange * 0.4) + )} + +
+
+ + {Math.abs(store.pvzReturnsChange)} + +
+
+ )}
+ + {/* Второй уровень - детализация по товарам */} + {expandedStores.has(store.id) && ( +
+ {/* Статическая часть - заголовки столбцов второго уровня */} +
+
+
+ Наименование +
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ + {/* Динамическая часть - данные по товарам (скроллируемая) */} +
+ {store.items?.map((item) => ( +
+ {/* Основная строка товара */} +
toggleItemExpansion(item.id)} + > +
+ {/* Наименование */} +
+
+
+
+ {item.name} + {item.variants && + item.variants.length > 0 && ( + + {item.variants.length} вар. + + )} +
+
+ {item.article} +
+
+
+ + {/* Продукты */} +
+
+ {formatNumber(item.productQuantity)} +
+
+ {item.productPlace || "-"} +
+
+ + {/* Товары */} +
+
+ {formatNumber(item.goodsQuantity)} +
+
+ {item.goodsPlace || "-"} +
+
+ + {/* Брак */} +
+
+ {formatNumber(item.defectsQuantity)} +
+
+ {item.defectsPlace || "-"} +
+
+ + {/* Расходники селлера */} +
+
+ {formatNumber( + item.sellerSuppliesQuantity + )} +
+
+ {item.sellerSuppliesPlace || "-"} +
+
+ + {/* Возвраты с ПВЗ */} +
+
+ {formatNumber(item.pvzReturnsQuantity)} +
+
+ {item.pvzReturnsPlace || "-"} +
+
+
+
+ + {/* Третий уровень - варианты товара */} + {expandedItems.has(item.id) && + item.variants && + item.variants.length > 0 && ( +
+ {/* Заголовки для вариантов */} +
+
+
+ Вариант +
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ Кол-во +
+
+ Место +
+
+
+
+ + {/* Данные по вариантам */} +
+ {item.variants.map((variant) => ( +
+
+ {/* Название варианта */} +
+
+
+ {variant.name} +
+
+ + {/* Продукты */} +
+
+ {formatNumber( + variant.productQuantity + )} +
+
+ {variant.productPlace || "-"} +
+
+ + {/* Товары */} +
+
+ {formatNumber( + variant.goodsQuantity + )} +
+
+ {variant.goodsPlace || "-"} +
+
+ + {/* Брак */} +
+
+ {formatNumber( + variant.defectsQuantity + )} +
+
+ {variant.defectsPlace || "-"} +
+
+ + {/* Расходники селлера */} +
+
+ {formatNumber( + variant.sellerSuppliesQuantity + )} +
+
+ {variant.sellerSuppliesPlace || + "-"} +
+
+ + {/* Возвраты с ПВЗ */} +
+
+ {formatNumber( + variant.pvzReturnsQuantity + )} +
+
+ {variant.pvzReturnsPlace || + "-"} +
+
+
+
+ ))} +
+
+ )} +
+ ))} +
+
+ )}
- )} -
- ))} + ); + }) + )}
diff --git a/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx index eb08303..f77e949 100644 --- a/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx +++ b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx @@ -4,6 +4,7 @@ import React, { useState } from "react"; import { useQuery } from "@apollo/client"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { useAuth } from "@/hooks/useAuth"; import { ChevronDown, ChevronRight, @@ -61,9 +62,12 @@ interface SupplyOrder { export function SuppliesConsumablesTab() { const [expandedOrders, setExpandedOrders] = useState>(new Set()); + const { user } = useAuth(); // Загружаем заказы поставок - const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS); + const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, { + fetchPolicy: "cache-and-network", // Всегда проверяем актуальные данные + }); const toggleOrderExpansion = (orderId: string) => { const newExpanded = new Set(expandedOrders); @@ -75,8 +79,11 @@ export function SuppliesConsumablesTab() { setExpandedOrders(newExpanded); }; - // Получаем данные заказов поставок - const supplyOrders: SupplyOrder[] = data?.supplyOrders || []; + // Получаем данные заказов поставок и фильтруем только заказы созданные текущим селлером + const allSupplyOrders: SupplyOrder[] = data?.supplyOrders || []; + const supplyOrders: SupplyOrder[] = allSupplyOrders.filter( + (order) => order.organization.id === user?.organization?.id + ); // Генерируем порядковые номера для заказов const ordersWithNumbers = supplyOrders.map((order, index) => ({ diff --git a/src/components/supplies/create-supply-page.tsx b/src/components/supplies/create-supply-page.tsx index e9f4451..d8963d3 100644 --- a/src/components/supplies/create-supply-page.tsx +++ b/src/components/supplies/create-supply-page.tsx @@ -18,13 +18,11 @@ import { } from "@/components/ui/select"; import { useQuery } from "@apollo/client"; import { apolloClient } from "@/lib/apollo-client"; -import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_LOGISTICS } from "@/graphql/queries"; import { - ArrowLeft, - Package, - CalendarIcon, - Building, -} from "lucide-react"; + GET_MY_COUNTERPARTIES, + GET_ORGANIZATION_LOGISTICS, +} from "@/graphql/queries"; +import { ArrowLeft, Package, CalendarIcon, Building } from "lucide-react"; // Компонент создания поставки товаров с новым интерфейсом @@ -47,16 +45,18 @@ export function CreateSupplyPage() { const [goodsVolume, setGoodsVolume] = useState(0); const [cargoPlaces, setCargoPlaces] = useState(0); const [goodsPrice, setGoodsPrice] = useState(0); - const [fulfillmentServicesPrice, setFulfillmentServicesPrice] = useState(0); + const [fulfillmentServicesPrice, setFulfillmentServicesPrice] = + useState(0); const [logisticsPrice, setLogisticsPrice] = useState(0); const [selectedServicesCost, setSelectedServicesCost] = useState(0); - const [selectedConsumablesCost, setSelectedConsumablesCost] = useState(0); + const [selectedConsumablesCost, setSelectedConsumablesCost] = + useState(0); const [hasItemsInSupply, setHasItemsInSupply] = useState(false); // Загружаем контрагентов-фулфилментов const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES); - // Фильтруем только фулфилмент организации + // Фильтруем только фулфилмент организации const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter( (org: Organization) => org.type === "FULFILLMENT" ); @@ -87,19 +87,28 @@ export function CreateSupplyPage() { }; // Функция для обновления информации о поставщиках (для расчета логистики) - const handleSuppliersUpdate = (suppliersData: any[]) => { + const handleSuppliersUpdate = (suppliersData: unknown[]) => { // Находим рынок из выбранного поставщика - const selectedSupplier = suppliersData.find(supplier => supplier.selected); - const supplierMarket = selectedSupplier?.market; - - console.log("Обновление поставщиков:", { selectedSupplier, supplierMarket, volume: goodsVolume }); - + const selectedSupplier = suppliersData.find( + (supplier: unknown) => (supplier as { selected?: boolean }).selected + ); + const supplierMarket = (selectedSupplier as { market?: string })?.market; + + console.log("Обновление поставщиков:", { + selectedSupplier, + supplierMarket, + volume: goodsVolume, + }); + // Пересчитываем логистику с учетом рынка поставщика calculateLogisticsPrice(goodsVolume, supplierMarket); }; // Функция для расчета логистики по рынку поставщика и объему - const calculateLogisticsPrice = async (volume: number, supplierMarket?: string) => { + const calculateLogisticsPrice = async ( + volume: number, + supplierMarket?: string + ) => { // Логистика рассчитывается ТОЛЬКО если есть: // 1. Выбранный фулфилмент // 2. Объем товаров > 0 @@ -110,22 +119,35 @@ export function CreateSupplyPage() { } try { - console.log(`Расчет логистики: ${supplierMarket} → ${selectedFulfillment}, объем: ${volume.toFixed(4)} м³`); - + console.log( + `Расчет логистики: ${supplierMarket} → ${selectedFulfillment}, объем: ${volume.toFixed( + 4 + )} м³` + ); + // Получаем логистику выбранного фулфилмента из БД const { data: logisticsData } = await apolloClient.query({ query: GET_ORGANIZATION_LOGISTICS, variables: { organizationId: selectedFulfillment }, - fetchPolicy: 'network-only' + fetchPolicy: "network-only", }); const logistics = logisticsData?.organizationLogistics || []; console.log(`Логистика фулфилмента ${selectedFulfillment}:`, logistics); // Ищем логистику для данного рынка - const logisticsRoute = logistics.find((route: any) => - route.fromLocation.toLowerCase().includes(supplierMarket.toLowerCase()) || - supplierMarket.toLowerCase().includes(route.fromLocation.toLowerCase()) + const logisticsRoute = logistics.find( + (route: { + fromLocation: string; + toLocation: string; + pricePerCubicMeter: number; + }) => + route.fromLocation + .toLowerCase() + .includes(supplierMarket.toLowerCase()) || + supplierMarket + .toLowerCase() + .includes(route.fromLocation.toLowerCase()) ); if (!logisticsRoute) { @@ -135,12 +157,21 @@ export function CreateSupplyPage() { } // Выбираем цену в зависимости от объема - const pricePerM3 = volume <= 1 ? logisticsRoute.priceUnder1m3 : logisticsRoute.priceOver1m3; + const pricePerM3 = + volume <= 1 + ? logisticsRoute.priceUnder1m3 + : logisticsRoute.priceOver1m3; const calculatedPrice = volume * pricePerM3; - - console.log(`Найдена логистика: ${logisticsRoute.fromLocation} → ${logisticsRoute.toLocation}`); - console.log(`Цена: ${pricePerM3}₽/м³ (${volume <= 1 ? 'до 1м³' : 'больше 1м³'}) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}₽`); - + + console.log( + `Найдена логистика: ${logisticsRoute.fromLocation} → ${logisticsRoute.toLocation}` + ); + console.log( + `Цена: ${pricePerM3}₽/м³ (${ + volume <= 1 ? "до 1м³" : "больше 1м³" + }) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}₽` + ); + setLogisticsPrice(calculatedPrice); } catch (error) { console.error("Error calculating logistics price:", error); @@ -149,7 +180,12 @@ export function CreateSupplyPage() { }; const getTotalSum = () => { - return goodsPrice + selectedServicesCost + selectedConsumablesCost + logisticsPrice; + return ( + goodsPrice + + selectedServicesCost + + selectedConsumablesCost + + logisticsPrice + ); }; const handleSupplyComplete = () => { @@ -258,7 +294,7 @@ export function CreateSupplyPage() { setCargoPlaces(parseInt(e.target.value) || 0)} + onChange={(e) => + setCargoPlaces(parseInt(e.target.value) || 0) + } placeholder="шт" className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs" /> @@ -311,7 +351,9 @@ export function CreateSupplyPage() {
- {goodsPrice > 0 ? formatCurrency(goodsPrice) : 'Рассчитывается автоматически'} + {goodsPrice > 0 + ? formatCurrency(goodsPrice) + : "Рассчитывается автоматически"}
@@ -323,7 +365,9 @@ export function CreateSupplyPage() {
- {selectedServicesCost > 0 ? formatCurrency(selectedServicesCost) : 'Выберите услуги'} + {selectedServicesCost > 0 + ? formatCurrency(selectedServicesCost) + : "Выберите услуги"}
@@ -335,7 +379,9 @@ export function CreateSupplyPage() {
- {selectedConsumablesCost > 0 ? formatCurrency(selectedConsumablesCost) : 'Выберите расходники'} + {selectedConsumablesCost > 0 + ? formatCurrency(selectedConsumablesCost) + : "Выберите расходники"}
@@ -347,12 +393,12 @@ export function CreateSupplyPage() {
- {logisticsPrice > 0 ? formatCurrency(logisticsPrice) : 'Выберите поставщика'} + {logisticsPrice > 0 + ? formatCurrency(logisticsPrice) + : "Выберите поставщика"}
- -
{/* 9. Итоговая сумма */} @@ -370,11 +416,21 @@ export function CreateSupplyPage() { {/* 10. Кнопка создания поставки */}
) : ( - 'Создать поставку' + "Создать поставку" )} diff --git a/src/components/supplies/direct-supply-creation.tsx b/src/components/supplies/direct-supply-creation.tsx index c914090..987a41e 100644 --- a/src/components/supplies/direct-supply-creation.tsx +++ b/src/components/supplies/direct-supply-creation.tsx @@ -113,7 +113,7 @@ interface DirectSupplyCreationProps { onItemsCountChange?: (hasItems: boolean) => void; onConsumablesCostChange?: (cost: number) => void; onVolumeChange?: (totalVolume: number) => void; - onSuppliersChange?: (suppliers: any[]) => void; + onSuppliersChange?: (suppliers: unknown[]) => void; } export function DirectSupplyCreation({ @@ -888,12 +888,12 @@ export function DirectSupplyCreation({ // Проверяем есть ли уже выбранные поставщики и уведомляем родителя if (onSuppliersChange && supplyItems.length > 0) { - const suppliersInfo = suppliersData.supplySuppliers.map((supplier: any) => ({ + const suppliersInfo = suppliersData.supplySuppliers.map((supplier: { id: string; selected?: boolean }) => ({ ...supplier, selected: supplyItems.some(item => item.supplierId === supplier.id) })); - if (suppliersInfo.some((s: any) => s.selected)) { + if (suppliersInfo.some((s: { selected?: boolean }) => s.selected)) { console.log("Найдены выбранные поставщики при загрузке:", suppliersInfo); // Вызываем асинхронно чтобы не обновлять состояние во время рендера diff --git a/src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx index 34accf4..4c8ec00 100644 --- a/src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx +++ b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx @@ -4,7 +4,9 @@ import React, { useState, useEffect } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { FulfillmentGoodsTab } from "./fulfillment-goods-tab"; import { RealSupplyOrdersTab } from "./real-supply-orders-tab"; +import { SellerSupplyOrdersTab } from "./seller-supply-orders-tab"; import { PvzReturnsTab } from "./pvz-returns-tab"; +import { useAuth } from "@/hooks/useAuth"; interface FulfillmentSuppliesTabProps { defaultSubTab?: string; @@ -14,6 +16,7 @@ export function FulfillmentSuppliesTab({ defaultSubTab, }: FulfillmentSuppliesTabProps) { const [activeSubTab, setActiveSubTab] = useState("goods"); + const { user } = useAuth(); // Устанавливаем активную подвкладку при получении defaultSubTab useEffect(() => { @@ -22,6 +25,9 @@ export function FulfillmentSuppliesTab({ } }, [defaultSubTab]); + // Определяем тип организации для выбора правильного компонента + const isWholesale = user?.organization?.type === "WHOLESALE"; + return (
- + {isWholesale ? : } diff --git a/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx b/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx index f72ccf5..290a86f 100644 --- a/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx +++ b/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx @@ -3,18 +3,28 @@ import React, { useState } from "react"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { StatsCard } from "../ui/stats-card"; import { StatsGrid } from "../ui/stats-grid"; -import { useQuery } from "@apollo/client"; +import { useQuery, useMutation } from "@apollo/client"; import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; +import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; import { Calendar, - MapPin, Building2, TrendingUp, DollarSign, Wrench, Package2, + ChevronDown, + ChevronRight, + User, + CheckCircle, + XCircle, + Clock, + Truck, } from "lucide-react"; interface SupplyOrderItem { @@ -36,10 +46,12 @@ interface SupplyOrderItem { interface SupplyOrder { id: string; + organizationId: string; deliveryDate: string; - status: string; + status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED"; totalAmount: number; totalItems: number; + fulfillmentCenterId?: string; createdAt: string; updatedAt: string; partner: { @@ -57,15 +69,53 @@ interface SupplyOrder { fullName?: string; type: string; }; + fulfillmentCenter?: { + id: string; + name?: string; + fullName?: string; + type: string; + }; items: SupplyOrderItem[]; } export function RealSupplyOrdersTab() { const [expandedOrders, setExpandedOrders] = useState>(new Set()); + const { user } = useAuth(); - const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS); + const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, { + fetchPolicy: "cache-and-network", + notifyOnNetworkStatusChange: true, + }); - const supplyOrders: SupplyOrder[] = data?.supplyOrders || []; + // Мутация для обновления статуса заказа + const [updateSupplyOrderStatus, { loading: updating }] = useMutation( + UPDATE_SUPPLY_ORDER_STATUS, + { + onCompleted: (data) => { + if (data.updateSupplyOrderStatus.success) { + toast.success(data.updateSupplyOrderStatus.message); + refetch(); // Обновляем список заказов + } else { + toast.error(data.updateSupplyOrderStatus.message); + } + }, + onError: (error) => { + console.error("Error updating supply order status:", error); + toast.error("Ошибка при обновлении статуса заказа"); + }, + } + ); + + // Получаем ID текущей организации (оптовика) + const currentOrganizationId = user?.organization?.id; + + // Фильтруем заказы где текущая организация является поставщиком (заказы К оптовику) + const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( + (order: SupplyOrder) => { + // Показываем заказы где текущий оптовик указан как поставщик (partnerId) + return order.partner.id === currentOrganizationId; + } + ); const toggleOrderExpansion = (orderId: string) => { const newExpanded = new Set(expandedOrders); @@ -77,36 +127,59 @@ export function RealSupplyOrdersTab() { setExpandedOrders(newExpanded); }; - const getStatusBadge = (status: string) => { - const statusMap: Record = { - CREATED: { - label: "Создан", + const handleStatusUpdate = async ( + orderId: string, + newStatus: SupplyOrder["status"] + ) => { + try { + await updateSupplyOrderStatus({ + variables: { + id: orderId, + status: newStatus, + }, + }); + } catch (error) { + console.error("Error updating status:", error); + } + }; + + const getStatusBadge = (status: SupplyOrder["status"]) => { + const statusMap = { + PENDING: { + label: "Ожидает одобрения", color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + icon: Clock, }, CONFIRMED: { - label: "Подтвержден", + label: "Одобрена", color: "bg-green-500/20 text-green-300 border-green-500/30", + icon: CheckCircle, }, - IN_PROGRESS: { - label: "В процессе", + IN_TRANSIT: { + label: "В пути", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + icon: Truck, }, DELIVERED: { - label: "Доставлен", + label: "Доставлена", color: "bg-purple-500/20 text-purple-300 border-purple-500/30", + icon: Package2, }, CANCELLED: { - label: "Отменен", + label: "Отклонена", color: "bg-red-500/20 text-red-300 border-red-500/30", + icon: XCircle, }, }; - - const { label, color } = statusMap[status] || { - label: status, - color: "bg-gray-500/20 text-gray-300 border-gray-500/30", - }; - - return {label}; + + const { label, color, icon: Icon } = statusMap[status]; + + return ( + + + {label} + + ); }; const formatCurrency = (amount: number) => { @@ -135,17 +208,31 @@ export function RealSupplyOrdersTab() { }); }; - // Статистика - const totalOrders = supplyOrders.length; - const totalAmount = supplyOrders.reduce((sum, order) => sum + order.totalAmount, 0); - const totalItems = supplyOrders.reduce((sum, order) => sum + order.totalItems, 0); - const completedOrders = supplyOrders.filter(order => order.status === "DELIVERED").length; + // Статистика для оптовика + const totalOrders = incomingSupplyOrders.length; + const totalAmount = incomingSupplyOrders.reduce( + (sum, order) => sum + order.totalAmount, + 0 + ); + const totalItems = incomingSupplyOrders.reduce( + (sum, order) => sum + order.totalItems, + 0 + ); + const pendingOrders = incomingSupplyOrders.filter( + (order) => order.status === "PENDING" + ).length; + const approvedOrders = incomingSupplyOrders.filter( + (order) => order.status === "CONFIRMED" + ).length; + const inTransitOrders = incomingSupplyOrders.filter( + (order) => order.status === "IN_TRANSIT" + ).length; if (loading) { return (
- Загрузка заказов расходников... + Загрузка заявок...
); } @@ -155,7 +242,7 @@ export function RealSupplyOrdersTab() {
-

Ошибка загрузки заказов

+

Ошибка загрузки заявок

{error.message}

@@ -163,16 +250,43 @@ export function RealSupplyOrdersTab() { } return ( -
- {/* Статистика заказов расходников */} +
+ {/* Статистика входящих заявок */} + + + + + + - - - {/* Список заказов расходников */} - {supplyOrders.length === 0 ? ( + {/* Список входящих заявок */} + {incomingSupplyOrders.length === 0 ? (

- Пока нет заказов расходников + Пока нет заявок

- Создайте первый заказ расходников через кнопку "Создать поставку" + Здесь будут отображаться заявки от селлеров на поставку товаров

) : ( - -
+ +
+ - {supplyOrders.map((order) => { + {incomingSupplyOrders.map((order) => { const isOrderExpanded = expandedOrders.has(order.id); return ( {/* Основная строка заказа */} - toggleOrderExpansion(order.id)} - > + @@ -299,16 +418,74 @@ export function RealSupplyOrdersTab() { + {/* Развернутая информация о заказе */} {isOrderExpanded && ( -
ID - Поставщик + Заказчик Дата поставки - Дата создания + Дата заявки Количество @@ -241,34 +346,48 @@ export function RealSupplyOrdersTab() { Статус + Действия +
- - {order.id.slice(-8)} - +
+ + + {order.id.slice(-8)} + +
- + - {order.partner.name || order.partner.fullName || "Поставщик"} + {order.organization.name || + order.organization.fullName || + "Заказчик"}

- ИНН: {order.partner.inn} + Тип: {order.organization.type}

{getStatusBadge(order.status)} +
+ {order.status === "PENDING" && ( + <> + + + + )} + {order.status === "CONFIRMED" && ( + + )} + {order.status === "CANCELLED" && ( + + Отклонена + + )} + {order.status === "IN_TRANSIT" && ( + + В пути + + )} + {order.status === "DELIVERED" && ( + + Доставлена + + )} +
+
+

- Состав заказа: + Состав заявки:

{order.items.map((item) => ( @@ -364,4 +541,4 @@ export function RealSupplyOrdersTab() { )}
); -} \ No newline at end of file +} diff --git a/src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx b/src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx new file mode 100644 index 0000000..91fd18b --- /dev/null +++ b/src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx @@ -0,0 +1,418 @@ +"use client"; + +import React, { useState } from "react"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { useQuery } from "@apollo/client"; +import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; +import { useAuth } from "@/hooks/useAuth"; +import { + Calendar, + Building2, + TrendingUp, + DollarSign, + Wrench, + Package2, + ChevronDown, + ChevronRight, + User, + Clock, + Truck, + Box, +} from "lucide-react"; + +// Типы данных для заказов поставок расходников +interface SupplyOrderItem { + id: string; + quantity: number; + price: number; + totalPrice: number; + product: { + id: string; + name: string; + article?: string; + description?: string; + category?: { + id: string; + name: string; + }; + }; +} + +interface SupplyOrder { + id: string; + deliveryDate: string; + status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED"; + totalAmount: number; + totalItems: number; + createdAt: string; + updatedAt: string; + partner: { + id: string; + name?: string; + fullName?: string; + inn?: string; + address?: string; + phones?: string[]; + emails?: string[]; + }; + organization: { + id: string; + name?: string; + fullName?: string; + type: string; + }; + fulfillmentCenter?: { + id: string; + name?: string; + fullName?: string; + }; + items: SupplyOrderItem[]; +} + +export function SellerSupplyOrdersTab() { + const [expandedOrders, setExpandedOrders] = useState>(new Set()); + const { user } = useAuth(); + + // Загружаем заказы поставок + const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, { + fetchPolicy: "cache-and-network", + }); + + const toggleOrderExpansion = (orderId: string) => { + const newExpanded = new Set(expandedOrders); + if (newExpanded.has(orderId)) { + newExpanded.delete(orderId); + } else { + newExpanded.add(orderId); + } + setExpandedOrders(newExpanded); + }; + + // Фильтруем заказы созданные текущим селлером + const sellerOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( + (order: SupplyOrder) => { + return order.organization.id === user?.organization?.id; + } + ); + + const getStatusBadge = (status: SupplyOrder["status"]) => { + const statusMap = { + PENDING: { + label: "Ожидает одобрения", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + }, + CONFIRMED: { + label: "Одобрена", + color: "bg-green-500/20 text-green-300 border-green-500/30", + }, + IN_TRANSIT: { + label: "В пути", + color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + }, + DELIVERED: { + label: "Доставлена", + color: "bg-purple-500/20 text-purple-300 border-purple-500/30", + }, + CANCELLED: { + label: "Отменена", + color: "bg-red-500/20 text-red-300 border-red-500/30", + }, + }; + const { label, color } = statusMap[status]; + return {label}; + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }).format(amount); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("ru-RU", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + }; + + // Статистика для селлера + const totalOrders = sellerOrders.length; + const totalAmount = sellerOrders.reduce( + (sum, order) => sum + order.totalAmount, + 0 + ); + const totalItems = sellerOrders.reduce( + (sum, order) => sum + order.totalItems, + 0 + ); + const pendingOrders = sellerOrders.filter( + (order) => order.status === "PENDING" + ).length; + const approvedOrders = sellerOrders.filter( + (order) => order.status === "CONFIRMED" + ).length; + const inTransitOrders = sellerOrders.filter( + (order) => order.status === "IN_TRANSIT" + ).length; + const deliveredOrders = sellerOrders.filter( + (order) => order.status === "DELIVERED" + ).length; + + if (loading) { + return ( +
+
+ Загрузка заказов... +
+ ); + } + + if (error) { + return ( +
+
+ +

Ошибка загрузки заказов

+

{error.message}

+
+
+ ); + } + + return ( +
+ {/* Статистика заказов поставок селлера */} +
+ +
+
+ +
+
+

Всего заказов

+

{totalOrders}

+
+
+
+ + +
+
+ +
+
+

Общая сумма

+

+ {formatCurrency(totalAmount)} +

+
+
+
+ + +
+
+ +
+
+

Ожидают

+

{pendingOrders}

+
+
+
+ + +
+
+ +
+
+

Доставлено

+

{deliveredOrders}

+
+
+
+
+ + {/* Таблица заказов поставок */} + +
+ + + + + + + + + + + + + + {sellerOrders.length === 0 ? ( + + + + ) : ( + sellerOrders.map((order, index) => { + const isOrderExpanded = expandedOrders.has(order.id); + const orderNumber = sellerOrders.length - index; + + return ( + + {/* Основная строка заказа поставки */} + toggleOrderExpansion(order.id)} + > + + + + + + + + + + {/* Развернутая информация о заказе */} + {isOrderExpanded && ( + + + + )} + + ); + }) + )} + +
+ № + + Поставщик + + Дата доставки + + Позиций + + Сумма + + Фулфилмент + + Статус +
+
+ +

Заказов поставок пока нет

+

+ Создайте первый заказ поставки расходников +

+
+
+
+ {isOrderExpanded ? ( + + ) : ( + + )} + + {orderNumber} + +
+
+
+ +
+

+ {order.partner.name || + order.partner.fullName || + "Не указан"} +

+ {order.partner.inn && ( +

+ ИНН: {order.partner.inn} +

+ )} +
+
+
+
+ + + {formatDate(order.deliveryDate)} + +
+
+ + {order.totalItems} + + + + {formatCurrency(order.totalAmount)} + + +
+ + + {order.fulfillmentCenter?.name || + order.fulfillmentCenter?.fullName || + "Не указан"} + +
+
{getStatusBadge(order.status)}
+
+
+

+ Состав заказа: +

+
+ {order.items.map((item) => ( +
+
+ +
+

+ {item.product.name} +

+ {item.product.category && ( +

+ {item.product.category.name} +

+ )} +
+
+
+

+ {item.quantity} шт ×{" "} + {formatCurrency(item.price)} +

+

+ = {formatCurrency(item.totalPrice)} +

+
+
+ ))} +
+
+ + Создан: {formatDate(order.createdAt)} + + + Итого: {formatCurrency(order.totalAmount)} + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/wb-warehouse/stock-table-row.tsx b/src/components/wb-warehouse/stock-table-row.tsx index 81492f7..791032b 100644 --- a/src/components/wb-warehouse/stock-table-row.tsx +++ b/src/components/wb-warehouse/stock-table-row.tsx @@ -1,98 +1,127 @@ -"use client" +"use client"; -import React from 'react' -import { Package } from 'lucide-react' +import React from "react"; +import { Package } from "lucide-react"; // Интерфейсы (можно будет вынести в отдельный файл types.ts) interface WBStockInfo { - warehouseId: number - warehouseName: string - quantity: number - quantityFull: number - inWayToClient: number - inWayFromClient: number + warehouseId: number; + warehouseName: string; + quantity: number; + quantityFull: number; + inWayToClient: number; + inWayFromClient: number; } interface WBStock { - nmId: number - vendorCode: string - title: string - brand: string - price: number - stocks: WBStockInfo[] - totalQuantity: number - totalReserved: number - photos: any[] - mediaFiles: any[] - characteristics: any[] - subjectName: string - description: string + nmId: number; + vendorCode: string; + title: string; + brand: string; + price: number; + stocks: WBStockInfo[]; + totalQuantity: number; + totalReserved: number; + photos: unknown[]; + mediaFiles: unknown[]; + characteristics: unknown[]; + subjectName: string; + description: string; } interface StockTableRowProps { - item: WBStock + item: WBStock; } export function StockTableRow({ item }: StockTableRowProps) { // Функция для получения изображений карточки const getCardImages = (item: WBStock) => { - const fallbackUrl = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp` - + const fallbackUrl = `https://basket-${String(item.nmId).slice( + 0, + 2 + )}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String( + item.nmId + ).slice(0, -3)}/${item.nmId}/images/big/1.webp`; + // Проверяем photos if (item.photos && item.photos.length > 0) { - return item.photos.map(photo => photo.big || photo.c516x688 || photo.c246x328 || photo.square || photo.tm || fallbackUrl) + return item.photos.map( + (photo) => + photo.big || + photo.c516x688 || + photo.c246x328 || + photo.square || + photo.tm || + fallbackUrl + ); } - + // Проверяем mediaFiles if (item.mediaFiles && item.mediaFiles.length > 0) { - return item.mediaFiles.map(media => media.big || media.c516x688 || media.c246x328 || media.square || media.tm || fallbackUrl) + return item.mediaFiles.map( + (media) => + media.big || + media.c516x688 || + media.c246x328 || + media.square || + media.tm || + fallbackUrl + ); } - - return [fallbackUrl] - } - - const getStockStatus = (quantity: number) => { - if (quantity === 0) return { - color: 'text-red-400', - bgColor: 'bg-red-500/10', - label: 'Нет в наличии' - } - if (quantity < 10) return { - color: 'text-orange-400', - bgColor: 'bg-orange-500/10', - label: 'Мало' - } - return { - color: 'text-green-400', - bgColor: 'bg-green-500/10', - label: 'В наличии' - } - } - const stockStatus = getStockStatus(item.totalQuantity) - const images = getCardImages(item) - const mainImage = images[0] || null + return [fallbackUrl]; + }; + + const getStockStatus = (quantity: number) => { + if (quantity === 0) + return { + color: "text-red-400", + bgColor: "bg-red-500/10", + label: "Нет в наличии", + }; + if (quantity < 10) + return { + color: "text-orange-400", + bgColor: "bg-orange-500/10", + label: "Мало", + }; + return { + color: "text-green-400", + bgColor: "bg-green-500/10", + label: "В наличии", + }; + }; + + const stockStatus = getStockStatus(item.totalQuantity); + const images = getCardImages(item); + const mainImage = images[0] || null; // Отбираем ключевые характеристики для отображения в таблице - const keyCharacteristics = item.characteristics?.slice(0, 3) || [] + const keyCharacteristics = item.characteristics?.slice(0, 3) || []; return (
{/* Основная строка товара */} -
+
{/* Товар (3 колонки) */}
{mainImage ? ( - {item.title} { - const target = e.target as HTMLImageElement - target.src = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp` + const target = e.target as HTMLImageElement; + target.src = `https://basket-${String(item.nmId).slice( + 0, + 2 + )}.wbbasket.ru/vol${String(item.nmId).slice( + 0, + -5 + )}/part${String(item.nmId).slice(0, -3)}/${ + item.nmId + }/images/big/1.webp`; }} /> ) : ( @@ -104,16 +133,14 @@ export function StockTableRow({ item }: StockTableRowProps) {
- {item.brand || 'Без бренда'} + {item.brand || "Без бренда"} #{item.nmId}

{item.title}

-
- {item.vendorCode} -
+
{item.vendorCode}
@@ -123,7 +150,9 @@ export function StockTableRow({ item }: StockTableRowProps) {
{item.totalQuantity.toLocaleString()}
-
+
{stockStatus.label}
@@ -164,16 +193,22 @@ export function StockTableRow({ item }: StockTableRowProps) {
{keyCharacteristics.map((char, index) => (
- {char.name}: + + {char.name}: + - {Array.isArray(char.value) ? char.value.join(', ') : String(char.value)} + {Array.isArray(char.value) + ? char.value.join(", ") + : String(char.value)}
))} {item.subjectName && (
Категория: - {item.subjectName} + + {item.subjectName} +
)}
@@ -184,38 +219,46 @@ export function StockTableRow({ item }: StockTableRowProps) {
{item.stocks.map((stock, stockIndex) => ( -
{/* Название города */}
{stock.warehouseName}
- + {/* Цифры */}
-
0 ? 'text-green-400' : 'text-white/30'}`}> +
0 ? "text-green-400" : "text-white/30" + }`} + > {stock.quantity}
остаток
- + {(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && ( <>
- + {stock.inWayToClient > 0 && (
-
{stock.inWayToClient}
+
+ {stock.inWayToClient} +
к клиенту
)} - + {stock.inWayFromClient > 0 && (
-
{stock.inWayFromClient}
+
+ {stock.inWayFromClient} +
от клиента
)} @@ -227,5 +270,5 @@ export function StockTableRow({ item }: StockTableRowProps) {
- ) -} \ No newline at end of file + ); +} diff --git a/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx b/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx index 3dc13c7..acb5ddd 100644 --- a/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx +++ b/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx @@ -1,261 +1,295 @@ -"use client" +"use client"; -import React, { useState, useEffect } from 'react' -import { useAuth } from '@/hooks/useAuth' -import { Sidebar } from '@/components/dashboard/sidebar' -import { useSidebar } from '@/hooks/useSidebar' -import { Card } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { WildberriesService } from '@/services/wildberries-service' -import { toast } from 'sonner' -import { StatsCards } from './stats-cards' -import { SearchBar } from './search-bar' -import { TableHeader } from './table-header' -import { LoadingSkeleton } from './loading-skeleton' -import { StockTableRow } from './stock-table-row' -import { TrendingUp, Package } from 'lucide-react' +import React, { useState, useEffect } from "react"; +import { useAuth } from "@/hooks/useAuth"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useSidebar } from "@/hooks/useSidebar"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { WildberriesService } from "@/services/wildberries-service"; +import { toast } from "sonner"; +import { StatsCards } from "./stats-cards"; +import { SearchBar } from "./search-bar"; +import { TableHeader } from "./table-header"; +import { LoadingSkeleton } from "./loading-skeleton"; +import { StockTableRow } from "./stock-table-row"; +import { TrendingUp, Package } from "lucide-react"; interface WBStock { - nmId: number - vendorCode: string - title: string - brand: string - price: number + nmId: number; + vendorCode: string; + title: string; + brand: string; + price: number; stocks: Array<{ - warehouseId: number - warehouseName: string - quantity: number - quantityFull: number - inWayToClient: number - inWayFromClient: number - }> - totalQuantity: number - totalReserved: number + warehouseId: number; + warehouseName: string; + quantity: number; + quantityFull: number; + inWayToClient: number; + inWayFromClient: number; + }>; + totalQuantity: number; + totalReserved: number; photos?: Array<{ - big?: string - c246x328?: string - c516x688?: string - square?: string - tm?: string - }> - mediaFiles?: string[] + big?: string; + c246x328?: string; + c516x688?: string; + square?: string; + tm?: string; + }>; + mediaFiles?: string[]; characteristics?: Array<{ - name: string - value: string | string[] - }> - subjectName: string - description: string + name: string; + value: string | string[]; + }>; + subjectName: string; + description: string; } interface WBWarehouse { - id: number - name: string - cargoType: number - deliveryType: number + id: number; + name: string; + cargoType: number; + deliveryType: number; } export function WBWarehouseDashboard() { - const { user } = useAuth() - const { isCollapsed, getSidebarMargin } = useSidebar() - - const [stocks, setStocks] = useState([]) - const [warehouses, setWarehouses] = useState([]) - const [loading, setLoading] = useState(false) - const [searchTerm, setSearchTerm] = useState('') - - // Статистика - const [totalProducts, setTotalProducts] = useState(0) - const [totalStocks, setTotalStocks] = useState(0) - const [totalReserved, setTotalReserved] = useState(0) - const [totalFromClient, setTotalFromClient] = useState(0) - const [activeWarehouses, setActiveWarehouses] = useState(0) - - // Analytics data - const [analyticsData, setAnalyticsData] = useState([]) + const { user } = useAuth(); + const { isCollapsed, getSidebarMargin } = useSidebar(); - const hasWBApiKey = user?.wildberriesApiKey + const [stocks, setStocks] = useState([]); + const [warehouses, setWarehouses] = useState([]); + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + + // Статистика + const [totalProducts, setTotalProducts] = useState(0); + const [totalStocks, setTotalStocks] = useState(0); + const [totalReserved, setTotalReserved] = useState(0); + const [totalFromClient, setTotalFromClient] = useState(0); + const [activeWarehouses, setActiveWarehouses] = useState(0); + + // Analytics data + const [analyticsData, setAnalyticsData] = useState([]); + + const hasWBApiKey = user?.wildberriesApiKey; // Комбинирование карточек с индивидуальными данными аналитики - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => { - const stocksMap = new Map() - - // Создаем карту аналитических данных для быстрого поиска - const analyticsMap = new Map() // Map nmId to its analytics data - analyticsResults.forEach(result => { - analyticsMap.set(result.nmId, result.data) - }) + const combineCardsWithAnalytics = ( + cards: unknown[], + analyticsResults: unknown[] + ) => { + const stocksMap = new Map(); - cards.forEach(card => { + // Создаем карту аналитических данных для быстрого поиска + const analyticsMap = new Map(); // Map nmId to its analytics data + analyticsResults.forEach((result) => { + analyticsMap.set(result.nmId, result.data); + }); + + cards.forEach((card) => { const stock: WBStock = { nmId: card.nmID, - vendorCode: String(card.vendorCode || card.supplierVendorCode || ''), + vendorCode: String(card.vendorCode || card.supplierVendorCode || ""), title: String(card.title || card.object || `Товар ${card.nmID}`), - brand: String(card.brand || ''), + brand: String(card.brand || ""), price: 0, stocks: [], totalQuantity: 0, totalReserved: 0, photos: Array.isArray(card.photos) ? card.photos : [], mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [], - characteristics: Array.isArray(card.characteristics) ? card.characteristics : [], - subjectName: String(card.subjectName || card.object || ''), - description: String(card.description || '') - } - + characteristics: Array.isArray(card.characteristics) + ? card.characteristics + : [], + subjectName: String(card.subjectName || card.object || ""), + description: String(card.description || ""), + }; + if (card.sizes && card.sizes.length > 0) { - stock.price = Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0 + stock.price = + Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0; } - - const analyticsData = analyticsMap.get(card.nmID) + + const analyticsData = analyticsMap.get(card.nmID); if (analyticsData?.data?.regions) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - analyticsData.data.regions.forEach((region: any) => { + ( + analyticsData.data.regions as { + offices: { + officeID: number; + officeName: string; + metrics: { stockCount: number }; + }[]; + }[] + ).forEach((region) => { if (region.offices && region.offices.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - region.offices.forEach((office: any) => { + region.offices.forEach((office) => { stock.stocks.push({ warehouseId: office.officeID, warehouseName: office.officeName, quantity: office.metrics?.stockCount || 0, quantityFull: office.metrics?.stockCount || 0, inWayToClient: office.metrics?.toClientCount || 0, - inWayFromClient: office.metrics?.fromClientCount || 0 - }) - - stock.totalQuantity += office.metrics?.stockCount || 0 - stock.totalReserved += office.metrics?.toClientCount || 0 - }) + inWayFromClient: office.metrics?.fromClientCount || 0, + }); + + stock.totalQuantity += office.metrics?.stockCount || 0; + stock.totalReserved += office.metrics?.toClientCount || 0; + }); } - }) + }); } - - stocksMap.set(card.nmID, stock) - }) - - return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity) - } + + stocksMap.set(card.nmID, stock); + }); + + return Array.from(stocksMap.values()).sort( + (a, b) => b.totalQuantity - a.totalQuantity + ); + }; // Извлечение информации о складах из данных - const extractWarehousesFromStocks = (stocksData: WBStock[]): WBWarehouse[] => { - const warehousesMap = new Map() - - stocksData.forEach(stock => { - stock.stocks.forEach(stockInfo => { + const extractWarehousesFromStocks = ( + stocksData: WBStock[] + ): WBWarehouse[] => { + const warehousesMap = new Map(); + + stocksData.forEach((stock) => { + stock.stocks.forEach((stockInfo) => { if (!warehousesMap.has(stockInfo.warehouseId)) { warehousesMap.set(stockInfo.warehouseId, { id: stockInfo.warehouseId, name: stockInfo.warehouseName, cargoType: 1, - deliveryType: 1 - }) + deliveryType: 1, + }); } - }) - }) - - return Array.from(warehousesMap.values()) - } + }); + }); + + return Array.from(warehousesMap.values()); + }; // Обновление статистики - const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => { - setTotalProducts(stocksData.length) - setTotalStocks(stocksData.reduce((sum, item) => sum + item.totalQuantity, 0)) - setTotalReserved(stocksData.reduce((sum, item) => sum + item.totalReserved, 0)) - - const totalFromClientCount = stocksData.reduce((sum, item) => - sum + item.stocks.reduce((stockSum, stock) => stockSum + stock.inWayFromClient, 0), 0 - ) - setTotalFromClient(totalFromClientCount) - + const updateStatistics = ( + stocksData: WBStock[], + warehousesData: WBWarehouse[] + ) => { + setTotalProducts(stocksData.length); + setTotalStocks( + stocksData.reduce((sum, item) => sum + item.totalQuantity, 0) + ); + setTotalReserved( + stocksData.reduce((sum, item) => sum + item.totalReserved, 0) + ); + + const totalFromClientCount = stocksData.reduce( + (sum, item) => + sum + + item.stocks.reduce( + (stockSum, stock) => stockSum + stock.inWayFromClient, + 0 + ), + 0 + ); + setTotalFromClient(totalFromClientCount); + const warehousesWithStock = new Set( - stocksData.flatMap(item => item.stocks.map(s => s.warehouseId)) - ) - setActiveWarehouses(warehousesWithStock.size) - } + stocksData.flatMap((item) => item.stocks.map((s) => s.warehouseId)) + ); + setActiveWarehouses(warehousesWithStock.size); + }; // Загрузка данных склада const loadWarehouseData = async () => { - if (!user?.wildberriesApiKey) return + if (!user?.wildberriesApiKey) return; - setLoading(true) + setLoading(true); try { - const apiToken = user.wildberriesApiKey - const wbService = new WildberriesService(apiToken) + const apiToken = user.wildberriesApiKey; + const wbService = new WildberriesService(apiToken); // 1. Получаем карточки товаров - const cards = await WildberriesService.getAllCards(apiToken).catch(() => []) - console.log('WB Warehouse: Loaded cards:', cards.length) + const cards = await WildberriesService.getAllCards(apiToken).catch( + () => [] + ); + console.log("WB Warehouse: Loaded cards:", cards.length); if (cards.length === 0) { - toast.error('Нет карточек товаров в WB') - return + toast.error("Нет карточек товаров в WB"); + return; } - const nmIds = cards.map(card => card.nmID).filter(id => id > 0) - console.log('WB Warehouse: NM IDs to process:', nmIds.length) + const nmIds = cards.map((card) => card.nmID).filter((id) => id > 0); + console.log("WB Warehouse: NM IDs to process:", nmIds.length); // 2. Получаем аналитику для каждого товара индивидуально - const analyticsResults = [] + const analyticsResults = []; for (const nmId of nmIds) { try { - console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`) + console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`); const result = await wbService.getStocksReportByOffices({ nmIDs: [nmId], - stockType: '' - }) - analyticsResults.push({ nmId, data: result }) - await new Promise(resolve => setTimeout(resolve, 1000)) + stockType: "", + }); + analyticsResults.push({ nmId, data: result }); + await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { - console.error(`WB Warehouse: Error fetching analytics for nmId ${nmId}:`, error) + console.error( + `WB Warehouse: Error fetching analytics for nmId ${nmId}:`, + error + ); } } - console.log('WB Warehouse: Analytics results:', analyticsResults.length) + console.log("WB Warehouse: Analytics results:", analyticsResults.length); // 3. Комбинируем данные - const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults) - console.log('WB Warehouse: Combined stocks:', combinedStocks.length) + const combinedStocks = combineCardsWithAnalytics(cards, analyticsResults); + console.log("WB Warehouse: Combined stocks:", combinedStocks.length); // 4. Извлекаем склады и обновляем статистику - const extractedWarehouses = extractWarehousesFromStocks(combinedStocks) - - setStocks(combinedStocks) - setWarehouses(extractedWarehouses) - updateStatistics(combinedStocks, extractedWarehouses) + const extractedWarehouses = extractWarehousesFromStocks(combinedStocks); - toast.success(`Загружено товаров: ${combinedStocks.length}`) - } catch (error: any) { - console.error('WB Warehouse: Error loading data:', error) - toast.error('Ошибка загрузки данных: ' + (error.message || 'Неизвестная ошибка')) + setStocks(combinedStocks); + setWarehouses(extractedWarehouses); + updateStatistics(combinedStocks, extractedWarehouses); + + toast.success(`Загружено товаров: ${combinedStocks.length}`); + } catch (error: unknown) { + console.error("Error loading warehouse data:", error); + toast.error("Ошибка при загрузке данных склада"); } finally { - setLoading(false) + setLoading(false); } - } + }; useEffect(() => { if (hasWBApiKey) { - loadWarehouseData() + loadWarehouseData(); } - }, [hasWBApiKey]) + }, [hasWBApiKey]); // Фильтрация товаров - const filteredStocks = stocks.filter(item => { - if (!searchTerm) return true - const search = searchTerm.toLowerCase() + const filteredStocks = stocks.filter((item) => { + if (!searchTerm) return true; + const search = searchTerm.toLowerCase(); return ( item.title.toLowerCase().includes(search) || String(item.nmId).includes(search) || item.brand.toLowerCase().includes(search) || item.vendorCode.toLowerCase().includes(search) - ) - }) + ); + }); return (
-
+
- {/* Результирующие вкладки */}
{analyticsData.map((warehouse) => ( - -
{warehouse.warehouseName}
+ +
+ {warehouse.warehouseName} +
К клиенту: - {warehouse.toClient} + + {warehouse.toClient} +
От клиента: - {warehouse.fromClient} + + {warehouse.fromClient} +
@@ -294,10 +337,7 @@ export function WBWarehouseDashboard() { )} {/* Поиск */} - + {/* Список товаров */}
@@ -309,10 +349,15 @@ export function WBWarehouseDashboard() { ) : !hasWBApiKey ? ( -

Настройте API Wildberries

-

Для просмотра остатков добавьте API ключ Wildberries в настройках

-
- ) -} \ No newline at end of file + ); +} diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts index d25790c..04d7715 100644 --- a/src/graphql/mutations.ts +++ b/src/graphql/mutations.ts @@ -1,4 +1,4 @@ -import { gql } from 'graphql-tag' +import { gql } from "graphql-tag"; export const SEND_SMS_CODE = gql` mutation SendSmsCode($phone: String!) { @@ -7,7 +7,7 @@ export const SEND_SMS_CODE = gql` message } } -` +`; export const VERIFY_SMS_CODE = gql` mutation VerifySmsCode($phone: String!, $code: String!) { @@ -56,7 +56,7 @@ export const VERIFY_SMS_CODE = gql` } } } -` +`; export const VERIFY_INN = gql` mutation VerifyInn($inn: String!) { @@ -71,10 +71,12 @@ export const VERIFY_INN = gql` } } } -` +`; export const REGISTER_FULFILLMENT_ORGANIZATION = gql` - mutation RegisterFulfillmentOrganization($input: FulfillmentRegistrationInput!) { + mutation RegisterFulfillmentOrganization( + $input: FulfillmentRegistrationInput! + ) { registerFulfillmentOrganization(input: $input) { success message @@ -119,7 +121,7 @@ export const REGISTER_FULFILLMENT_ORGANIZATION = gql` } } } -` +`; export const REGISTER_SELLER_ORGANIZATION = gql` mutation RegisterSellerOrganization($input: SellerRegistrationInput!) { @@ -167,7 +169,7 @@ export const REGISTER_SELLER_ORGANIZATION = gql` } } } -` +`; export const ADD_MARKETPLACE_API_KEY = gql` mutation AddMarketplaceApiKey($input: MarketplaceApiKeyInput!) { @@ -183,13 +185,13 @@ export const ADD_MARKETPLACE_API_KEY = gql` } } } -` +`; export const REMOVE_MARKETPLACE_API_KEY = gql` mutation RemoveMarketplaceApiKey($marketplace: MarketplaceType!) { removeMarketplaceApiKey(marketplace: $marketplace) } -` +`; export const UPDATE_USER_PROFILE = gql` mutation UpdateUserProfile($input: UpdateUserProfileInput!) { @@ -237,7 +239,7 @@ export const UPDATE_USER_PROFILE = gql` } } } -` +`; export const UPDATE_ORGANIZATION_BY_INN = gql` mutation UpdateOrganizationByInn($inn: String!) { @@ -285,12 +287,15 @@ export const UPDATE_ORGANIZATION_BY_INN = gql` } } } -` +`; // Мутации для контрагентов export const SEND_COUNTERPARTY_REQUEST = gql` mutation SendCounterpartyRequest($organizationId: ID!, $message: String) { - sendCounterpartyRequest(organizationId: $organizationId, message: $message) { + sendCounterpartyRequest( + organizationId: $organizationId + message: $message + ) { success message request { @@ -315,7 +320,7 @@ export const SEND_COUNTERPARTY_REQUEST = gql` } } } -` +`; export const RESPOND_TO_COUNTERPARTY_REQUEST = gql` mutation RespondToCounterpartyRequest($requestId: ID!, $accept: Boolean!) { @@ -344,24 +349,32 @@ export const RESPOND_TO_COUNTERPARTY_REQUEST = gql` } } } -` +`; export const CANCEL_COUNTERPARTY_REQUEST = gql` mutation CancelCounterpartyRequest($requestId: ID!) { cancelCounterpartyRequest(requestId: $requestId) } -` +`; export const REMOVE_COUNTERPARTY = gql` mutation RemoveCounterparty($organizationId: ID!) { removeCounterparty(organizationId: $organizationId) } -` +`; // Мутации для сообщений export const SEND_MESSAGE = gql` - mutation SendMessage($receiverOrganizationId: ID!, $content: String!, $type: MessageType = TEXT) { - sendMessage(receiverOrganizationId: $receiverOrganizationId, content: $content, type: $type) { + mutation SendMessage( + $receiverOrganizationId: ID! + $content: String! + $type: MessageType = TEXT + ) { + sendMessage( + receiverOrganizationId: $receiverOrganizationId + content: $content + type: $type + ) { success message messageData { @@ -403,11 +416,19 @@ export const SEND_MESSAGE = gql` } } } -` +`; export const SEND_VOICE_MESSAGE = gql` - mutation SendVoiceMessage($receiverOrganizationId: ID!, $voiceUrl: String!, $voiceDuration: Int!) { - sendVoiceMessage(receiverOrganizationId: $receiverOrganizationId, voiceUrl: $voiceUrl, voiceDuration: $voiceDuration) { + mutation SendVoiceMessage( + $receiverOrganizationId: ID! + $voiceUrl: String! + $voiceDuration: Int! + ) { + sendVoiceMessage( + receiverOrganizationId: $receiverOrganizationId + voiceUrl: $voiceUrl + voiceDuration: $voiceDuration + ) { success message messageData { @@ -449,11 +470,23 @@ export const SEND_VOICE_MESSAGE = gql` } } } -` +`; export const SEND_IMAGE_MESSAGE = gql` - mutation SendImageMessage($receiverOrganizationId: ID!, $fileUrl: String!, $fileName: String!, $fileSize: Int!, $fileType: String!) { - sendImageMessage(receiverOrganizationId: $receiverOrganizationId, fileUrl: $fileUrl, fileName: $fileName, fileSize: $fileSize, fileType: $fileType) { + mutation SendImageMessage( + $receiverOrganizationId: ID! + $fileUrl: String! + $fileName: String! + $fileSize: Int! + $fileType: String! + ) { + sendImageMessage( + receiverOrganizationId: $receiverOrganizationId + fileUrl: $fileUrl + fileName: $fileName + fileSize: $fileSize + fileType: $fileType + ) { success message messageData { @@ -495,11 +528,23 @@ export const SEND_IMAGE_MESSAGE = gql` } } } -` +`; export const SEND_FILE_MESSAGE = gql` - mutation SendFileMessage($receiverOrganizationId: ID!, $fileUrl: String!, $fileName: String!, $fileSize: Int!, $fileType: String!) { - sendFileMessage(receiverOrganizationId: $receiverOrganizationId, fileUrl: $fileUrl, fileName: $fileName, fileSize: $fileSize, fileType: $fileType) { + mutation SendFileMessage( + $receiverOrganizationId: ID! + $fileUrl: String! + $fileName: String! + $fileSize: Int! + $fileType: String! + ) { + sendFileMessage( + receiverOrganizationId: $receiverOrganizationId + fileUrl: $fileUrl + fileName: $fileName + fileSize: $fileSize + fileType: $fileType + ) { success message messageData { @@ -541,13 +586,13 @@ export const SEND_FILE_MESSAGE = gql` } } } -` +`; export const MARK_MESSAGES_AS_READ = gql` mutation MarkMessagesAsRead($conversationId: ID!) { markMessagesAsRead(conversationId: $conversationId) } -` +`; // Мутации для услуг export const CREATE_SERVICE = gql` @@ -566,7 +611,7 @@ export const CREATE_SERVICE = gql` } } } -` +`; export const UPDATE_SERVICE = gql` mutation UpdateService($id: ID!, $input: ServiceInput!) { @@ -584,13 +629,13 @@ export const UPDATE_SERVICE = gql` } } } -` +`; export const DELETE_SERVICE = gql` mutation DeleteService($id: ID!) { deleteService(id: $id) } -` +`; // Мутации для расходников export const CREATE_SUPPLY = gql` @@ -617,7 +662,7 @@ export const CREATE_SUPPLY = gql` } } } -` +`; export const UPDATE_SUPPLY = gql` mutation UpdateSupply($id: ID!, $input: SupplyInput!) { @@ -643,13 +688,13 @@ export const UPDATE_SUPPLY = gql` } } } -` +`; export const DELETE_SUPPLY = gql` mutation DeleteSupply($id: ID!) { deleteSupply(id: $id) } -` +`; // Мутация для заказа поставки расходников export const CREATE_SUPPLY_ORDER = gql` @@ -697,7 +742,7 @@ export const CREATE_SUPPLY_ORDER = gql` } } } -` +`; // Мутации для логистики export const CREATE_LOGISTICS = gql` @@ -717,7 +762,7 @@ export const CREATE_LOGISTICS = gql` } } } -` +`; export const UPDATE_LOGISTICS = gql` mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) { @@ -736,13 +781,13 @@ export const UPDATE_LOGISTICS = gql` } } } -` +`; export const DELETE_LOGISTICS = gql` mutation DeleteLogistics($id: ID!) { deleteLogistics(id: $id) } -` +`; // Мутации для товаров оптовика export const CREATE_PRODUCT = gql` @@ -775,7 +820,7 @@ export const CREATE_PRODUCT = gql` } } } -` +`; export const UPDATE_PRODUCT = gql` mutation UpdateProduct($id: ID!, $input: ProductInput!) { @@ -807,13 +852,13 @@ export const UPDATE_PRODUCT = gql` } } } -` +`; export const DELETE_PRODUCT = gql` mutation DeleteProduct($id: ID!) { deleteProduct(id: $id) } -` +`; // Мутации для корзины export const ADD_TO_CART = gql` @@ -849,7 +894,7 @@ export const ADD_TO_CART = gql` } } } -` +`; export const UPDATE_CART_ITEM = gql` mutation UpdateCartItem($productId: ID!, $quantity: Int!) { @@ -884,7 +929,7 @@ export const UPDATE_CART_ITEM = gql` } } } -` +`; export const REMOVE_FROM_CART = gql` mutation RemoveFromCart($productId: ID!) { @@ -919,13 +964,13 @@ export const REMOVE_FROM_CART = gql` } } } -` +`; export const CLEAR_CART = gql` mutation ClearCart { clearCart } -` +`; // Мутации для избранного export const ADD_TO_FAVORITES = gql` @@ -954,7 +999,7 @@ export const ADD_TO_FAVORITES = gql` } } } -` +`; export const REMOVE_FROM_FAVORITES = gql` mutation RemoveFromFavorites($productId: ID!) { @@ -982,7 +1027,7 @@ export const REMOVE_FROM_FAVORITES = gql` } } } -` +`; // Мутации для категорий export const CREATE_CATEGORY = gql` @@ -998,7 +1043,7 @@ export const CREATE_CATEGORY = gql` } } } -` +`; export const UPDATE_CATEGORY = gql` mutation UpdateCategory($id: ID!, $input: CategoryInput!) { @@ -1013,13 +1058,13 @@ export const UPDATE_CATEGORY = gql` } } } -` +`; export const DELETE_CATEGORY = gql` mutation DeleteCategory($id: ID!) { deleteCategory(id: $id) } -` +`; // Мутации для сотрудников export const CREATE_EMPLOYEE = gql` @@ -1048,7 +1093,7 @@ export const CREATE_EMPLOYEE = gql` } } } -` +`; export const UPDATE_EMPLOYEE = gql` mutation UpdateEmployee($id: ID!, $input: UpdateEmployeeInput!) { @@ -1081,19 +1126,19 @@ export const UPDATE_EMPLOYEE = gql` } } } -` +`; export const DELETE_EMPLOYEE = gql` mutation DeleteEmployee($id: ID!) { deleteEmployee(id: $id) } -` +`; export const UPDATE_EMPLOYEE_SCHEDULE = gql` mutation UpdateEmployeeSchedule($input: UpdateScheduleInput!) { updateEmployeeSchedule(input: $input) } -` +`; export const CREATE_WILDBERRIES_SUPPLY = gql` mutation CreateWildberriesSupply($input: CreateWildberriesSupplyInput!) { @@ -1110,7 +1155,7 @@ export const CREATE_WILDBERRIES_SUPPLY = gql` } } } -` +`; // Админ мутации export const ADMIN_LOGIN = gql` @@ -1130,13 +1175,13 @@ export const ADMIN_LOGIN = gql` } } } -` +`; export const ADMIN_LOGOUT = gql` mutation AdminLogout { adminLogout } -` +`; export const CREATE_SUPPLY_SUPPLIER = gql` mutation CreateSupplySupplier($input: CreateSupplySupplierInput!) { @@ -1156,4 +1201,46 @@ export const CREATE_SUPPLY_SUPPLIER = gql` } } } -` \ No newline at end of file +`; + +// Мутация для обновления статуса заказа поставки +export const UPDATE_SUPPLY_ORDER_STATUS = gql` + mutation UpdateSupplyOrderStatus($id: ID!, $status: SupplyOrderStatus!) { + updateSupplyOrderStatus(id: $id, status: $status) { + success + message + order { + id + status + deliveryDate + totalAmount + totalItems + partner { + id + name + fullName + } + items { + id + quantity + price + totalPrice + product { + id + name + article + description + price + quantity + images + mainImage + category { + id + name + } + } + } + } + } + } +`; diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index e5021a4..8aa0cf8 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -398,14 +398,18 @@ export const resolvers = { const suppliers = await prisma.supplySupplier.findMany({ where: { organizationId: currentUser.organization.id }, - orderBy: { createdAt: 'desc' } + orderBy: { createdAt: "desc" }, }); return suppliers; }, // Логистика конкретной организации - organizationLogistics: async (_: unknown, args: { organizationId: string }, context: Context) => { + organizationLogistics: async ( + _: unknown, + args: { organizationId: string }, + context: Context + ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, @@ -3259,38 +3263,38 @@ export const resolvers = { totalItems: totalItems, organizationId: currentUser.organization.id, fulfillmentCenterId: fulfillmentCenterId, - status: initialStatus as any, + status: initialStatus, items: { create: orderItems, }, }, - include: { - partner: { - include: { - users: true, + include: { + partner: { + include: { + users: true, + }, }, - }, - organization: { - include: { - users: true, + organization: { + include: { + users: true, + }, }, - }, - fulfillmentCenter: { - include: { - users: true, + fulfillmentCenter: { + include: { + users: true, + }, }, - }, - items: { - include: { - product: { - include: { - category: true, - organization: true, + items: { + include: { + product: { + include: { + category: true, + organization: true, + }, }, }, }, }, - }, }); // Создаем расходники на основе заказанных товаров @@ -4643,6 +4647,182 @@ export const resolvers = { }; } }, + + // Обновить статус заказа поставки + updateSupplyOrderStatus: async ( + _: unknown, + args: { + id: string; + status: + | "PENDING" + | "CONFIRMED" + | "IN_TRANSIT" + | "DELIVERED" + | "CANCELLED"; + }, + context: Context + ) => { + if (!context.user) { + throw new GraphQLError("Требуется авторизация", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true }, + }); + + if (!currentUser?.organization) { + throw new GraphQLError("У пользователя нет организации"); + } + + try { + // Находим заказ поставки + const existingOrder = await prisma.supplyOrder.findFirst({ + where: { + id: args.id, + OR: [ + { organizationId: currentUser.organization.id }, // Создатель заказа + { partnerId: currentUser.organization.id }, // Поставщик + { fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр + ], + }, + include: { + items: { + include: { + product: { + include: { + category: true, + }, + }, + }, + }, + partner: true, + fulfillmentCenter: true, + }, + }); + + if (!existingOrder) { + throw new GraphQLError("Заказ поставки не найден или нет доступа"); + } + + // Обновляем статус заказа + const updatedOrder = await prisma.supplyOrder.update({ + where: { id: args.id }, + data: { status: args.status }, + include: { + partner: true, + items: { + include: { + product: { + include: { + category: true, + }, + }, + }, + }, + }, + }); + + // Если статус изменился на DELIVERED, обновляем склад фулфилмента + if (args.status === "DELIVERED" && existingOrder.fulfillmentCenterId) { + console.log("🚚 Обновляем склад фулфилмента:", { + fulfillmentCenterId: existingOrder.fulfillmentCenterId, + itemsCount: existingOrder.items.length, + items: existingOrder.items.map((item) => ({ + productName: item.product.name, + quantity: item.quantity, + })), + }); + + // Обновляем расходники фулфилмента + for (const item of existingOrder.items) { + console.log("📦 Обрабатываем товар:", { + productName: item.product.name, + quantity: item.quantity, + fulfillmentCenterId: existingOrder.fulfillmentCenterId, + }); + + // Ищем существующий расходник + const existingSupply = await prisma.supply.findFirst({ + where: { + name: item.product.name, + organizationId: existingOrder.fulfillmentCenterId, + }, + }); + + console.log("🔍 Найден существующий расходник:", !!existingSupply); + + if (existingSupply) { + console.log("📈 Обновляем существующий расходник:", { + id: existingSupply.id, + oldStock: existingSupply.currentStock, + newStock: existingSupply.currentStock + item.quantity, + }); + + // Обновляем количество существующего расходника + await prisma.supply.update({ + where: { id: existingSupply.id }, + data: { + currentStock: existingSupply.currentStock + item.quantity, + status: "available", // Меняем статус на "доступен" + }, + }); + } else { + console.log("➕ Создаем новый расходник:", { + name: item.product.name, + quantity: item.quantity, + organizationId: existingOrder.fulfillmentCenterId, + }); + + // Создаем новый расходник + const newSupply = await prisma.supply.create({ + data: { + name: item.product.name, + description: + item.product.description || + `Поставка от ${existingOrder.partner.name}`, + price: item.price, + quantity: item.quantity, + unit: "шт", + category: item.product.category?.name || "Упаковка", + status: "available", + date: new Date(), + supplier: + existingOrder.partner.name || + existingOrder.partner.fullName || + "Не указан", + minStock: Math.round(item.quantity * 0.1), + currentStock: item.quantity, + organizationId: existingOrder.fulfillmentCenterId, + }, + }); + + console.log("✅ Создан новый расходник:", { + id: newSupply.id, + name: newSupply.name, + currentStock: newSupply.currentStock, + }); + } + } + + console.log("🎉 Склад фулфилмента успешно обновлен!"); + } + + return { + success: true, + message: `Статус заказа поставки обновлен на "${args.status}"`, + order: updatedOrder, + }; + } catch (error) { + console.error("Error updating supply order status:", error); + return { + success: false, + message: "Ошибка при обновлении статуса заказа поставки", + }; + } + }, }, // Резолверы типов diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 9539c8c..acb833f 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -181,6 +181,7 @@ export const typeDefs = gql` # Заказы поставок расходников createSupplyOrder(input: SupplyOrderInput!): SupplyOrderResponse! + updateSupplyOrderStatus(id: ID!, status: SupplyOrderStatus!): SupplyOrderResponse! # Работа с логистикой createLogistics(input: LogisticsInput!): LogisticsResponse!