Добавлен новый компонент для отображения бизнес-процессов в интерфейсе управления. Обновлен компонент UIKitSection для интеграции нового демо и улучшения навигации. Оптимизирована логика отображения данных и улучшена читаемость кода. Исправлены текстовые метки для повышения удобства использования.

This commit is contained in:
Veronika Smirnova
2025-07-27 20:10:39 +03:00
parent f198994400
commit ec28803549
17 changed files with 4304 additions and 1205 deletions

View File

@ -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() {
>
Навигация поставок
</TabsTrigger>
<TabsTrigger
value="business-processes"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 text-xs px-3 py-2"
>
Бизнес-процессы
</TabsTrigger>
</TabsList>
<TabsContent value="buttons" className="space-y-6">
@ -235,6 +242,10 @@ export function UIKitSection() {
<TabsContent value="supplies-navigation" className="space-y-6">
<SuppliesNavigationDemo />
</TabsContent>
<TabsContent value="business-processes" className="space-y-6">
<BusinessProcessesDemo />
</TabsContent>
</Tabs>
</div>
);

View File

@ -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 (
<div className="space-y-8">
{/* Заголовок */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Package className="h-6 w-6" />
Бизнес-процессы
</CardTitle>
<p className="text-white/70">
Схема всего проекта по кабинетам со связями между ними и детальная
визуализация бизнес-процесса поставки товаров
</p>
</CardHeader>
</Card>
{/* Общая схема проекта */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white text-xl">
🏗 Схема всего проекта по кабинетам
</CardTitle>
<p className="text-white/70">
Полная архитектура системы с типами кабинетов и их взаимосвязями
</p>
</CardHeader>
<CardContent>
{/* Mermaid диаграмма */}
<div className="mb-8 bg-white/5 rounded-lg p-4 border border-white/10">
<div className="text-center mb-4">
<h3 className="text-white font-medium mb-2">
Архитектурная схема проекта
</h3>
<p className="text-white/60 text-sm">
Кабинеты, модули и связи между ними
</p>
</div>
<div className="bg-white rounded-lg p-6 min-h-[500px] overflow-auto">
<div className="mermaid text-sm">
{`graph TB
%% Основные кабинеты
A["🏢 Админ<br/>Управление системой<br/>UI Kit, Пользователи, Категории"] --> B["🏪 Маркет<br/>Центральная площадка<br/>Поиск партнёров"]
%% Типы кабинетов
C["🛒 Селлер<br/>Продажи на маркетплейсах<br/>• Мои поставки<br/>• Склад WB<br/>• Статистика"]
D["📦 Фулфилмент<br/>Склады и логистика<br/>• Входящие поставки<br/>• Склад<br/>• Услуги<br/>• Сотрудники<br/>• Статистика"]
E["🚛 Логистика<br/>Логистические решения<br/>• Перевозки<br/>• Заявки"]
F["🏭 Оптовик<br/>Оптовые продажи<br/>• Отгрузки<br/>• Склад"]
%% Общие модули
G["💬 Мессенджер<br/>Общение между участниками<br/>• Чаты<br/>• Файлы<br/>• Голосовые"]
H["🤝 Партнёры<br/>Управление контрагентами<br/>• Заявки<br/>• Партнёрская сеть"]
I["⚙️ Настройки<br/>Профиль организации<br/>• API ключи<br/>• Данные компании"]
%% Связи между кабинетами
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`}
</div>
</div>
</div>
{/* Описание кабинетов */}
<div className="space-y-6">
<h3 className="text-white text-lg font-medium">Типы кабинетов</h3>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{cabinets.map((cabinet) => {
const IconComponent = cabinet.icon;
return (
<div
key={cabinet.id}
className="p-4 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 transition-all duration-200"
>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
<IconComponent className="h-5 w-5 text-white" />
</div>
<div>
<h4 className="text-white font-medium">
{cabinet.name}
</h4>
<p className="text-white/60 text-xs">
{cabinet.description}
</p>
</div>
</div>
<p className="text-white/70 text-sm mb-3">{cabinet.role}</p>
<div className="space-y-2">
<p className="text-white/50 text-xs font-medium">
Функции:
</p>
<div className="flex flex-wrap gap-1">
{cabinet.features.map((feature, index) => (
<Badge
key={index}
className={`${cabinet.color} border text-xs px-2 py-1`}
>
{feature}
</Badge>
))}
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Общие модули */}
<div className="space-y-6 mt-8">
<h3 className="text-white text-lg font-medium">Общие модули</h3>
<div className="grid md:grid-cols-3 gap-4">
{commonModules.map((module, index) => {
const IconComponent = module.icon;
return (
<div
key={index}
className="p-4 rounded-lg bg-gray-500/10 border border-gray-500/20 hover:bg-gray-500/20 transition-all duration-200"
>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-gray-500/20 flex items-center justify-center">
<IconComponent className="h-5 w-5 text-gray-300" />
</div>
<div>
<h4 className="text-white font-medium">
{module.name}
</h4>
<p className="text-white/60 text-xs">
{module.description}
</p>
</div>
</div>
<div className="space-y-2">
<p className="text-white/50 text-xs font-medium">
Функции:
</p>
<div className="flex flex-wrap gap-1 mb-2">
{module.features.map((feature, featureIndex) => (
<Badge
key={featureIndex}
className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs px-2 py-1"
>
{feature}
</Badge>
))}
</div>
<p className="text-white/40 text-xs">
<span className="font-medium">Доступ:</span>{" "}
{module.connectedTo.join(", ")}
</p>
</div>
</div>
);
})}
</div>
</div>
{/* Ключевые связи */}
<div className="space-y-4 mt-8">
<h3 className="text-white text-lg font-medium">
Ключевые бизнес-связи
</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
<h4 className="text-blue-400 font-medium mb-2 flex items-center gap-2">
<ArrowRight className="h-4 w-4" />
Процесс заказа
</h4>
<p className="text-white/70 text-sm">
Селлер Маркет Оптовик Фулфилмент Логистика
</p>
</div>
<div className="p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<h4 className="text-green-400 font-medium mb-2 flex items-center gap-2">
<MessageCircle className="h-4 w-4" />
Коммуникации
</h4>
<p className="text-white/70 text-sm">
Все кабинеты связаны через Мессенджер для обмена информацией
</p>
</div>
<div className="p-4 rounded-lg bg-purple-500/10 border border-purple-500/20">
<h4 className="text-purple-400 font-medium mb-2 flex items-center gap-2">
<Handshake className="h-4 w-4" />
Партнёрство
</h4>
<p className="text-white/70 text-sm">
Система заявок и управления контрагентами между всеми
участниками
</p>
</div>
<div className="p-4 rounded-lg bg-orange-500/10 border border-orange-500/20">
<h4 className="text-orange-400 font-medium mb-2 flex items-center gap-2">
<Settings className="h-4 w-4" />
Администрирование
</h4>
<p className="text-white/70 text-sm">
Админ-панель для управления пользователями, категориями и UI
Kit
</p>
</div>
</div>
</div>
{/* Упрощенная схема потоков данных */}
<div className="space-y-4 mt-8">
<h3 className="text-white text-lg font-medium">
Потоки данных между кабинетами
</h3>
<div className="bg-white/5 rounded-lg p-6 border border-white/10">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Основной поток заказа */}
<div className="space-y-4">
<h4 className="text-blue-400 font-medium text-center mb-4">
📋 Основной поток заказа
</h4>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center text-xs font-bold text-blue-400">
1
</div>
<div>
<p className="text-white text-sm font-medium">
Селлер создаёт заказ
</p>
<p className="text-white/60 text-xs">
Через маркет выбирает товары
</p>
</div>
</div>
<div className="flex justify-center">
<ArrowRight className="h-4 w-4 text-white/30 rotate-90" />
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-cyan-500/10 border border-cyan-500/20">
<div className="w-8 h-8 rounded-full bg-cyan-500/20 flex items-center justify-center text-xs font-bold text-cyan-400">
2
</div>
<div>
<p className="text-white text-sm font-medium">
Оптовик одобряет
</p>
<p className="text-white/60 text-xs">
Подтверждает наличие товара
</p>
</div>
</div>
<div className="flex justify-center">
<ArrowRight className="h-4 w-4 text-white/30 rotate-90" />
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
<div className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center text-xs font-bold text-red-400">
3
</div>
<div>
<p className="text-white text-sm font-medium">
Фулфилмент принимает
</p>
<p className="text-white/60 text-xs">
Готовится к приёмке товара
</p>
</div>
</div>
<div className="flex justify-center">
<ArrowRight className="h-4 w-4 text-white/30 rotate-90" />
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-orange-500/10 border border-orange-500/20">
<div className="w-8 h-8 rounded-full bg-orange-500/20 flex items-center justify-center text-xs font-bold text-orange-400">
4
</div>
<div>
<p className="text-white text-sm font-medium">
Логистика доставляет
</p>
<p className="text-white/60 text-xs">
Транспортирует товар
</p>
</div>
</div>
</div>
</div>
{/* Коммуникационный поток */}
<div className="space-y-4">
<h4 className="text-green-400 font-medium text-center mb-4">
💬 Коммуникации
</h4>
<div className="space-y-3">
<div className="p-4 rounded-lg bg-gray-500/10 border border-gray-500/20 text-center">
<MessageCircle className="h-8 w-8 text-green-400 mx-auto mb-2" />
<p className="text-white text-sm font-medium mb-2">
Мессенджер
</p>
<p className="text-white/60 text-xs mb-3">
Центр всех коммуникаций
</p>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-blue-400">Селлер</span>
<ArrowRight className="h-3 w-3 text-white/30" />
<span className="text-green-400">Мессенджер</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-red-400">Фулфилмент</span>
<ArrowRight className="h-3 w-3 text-white/30" />
<span className="text-green-400">Мессенджер</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-orange-400">Логистика</span>
<ArrowRight className="h-3 w-3 text-white/30" />
<span className="text-green-400">Мессенджер</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-cyan-400">Оптовик</span>
<ArrowRight className="h-3 w-3 text-white/30" />
<span className="text-green-400">Мессенджер</span>
</div>
</div>
</div>
</div>
</div>
{/* Управленческий поток */}
<div className="space-y-4">
<h4 className="text-purple-400 font-medium text-center mb-4">
🤝 Партнёрство
</h4>
<div className="space-y-3">
<div className="p-4 rounded-lg bg-gray-500/10 border border-gray-500/20 text-center">
<Handshake className="h-8 w-8 text-purple-400 mx-auto mb-2" />
<p className="text-white text-sm font-medium mb-2">
Система партнёрства
</p>
<p className="text-white/60 text-xs mb-3">
Управление контрагентами
</p>
<div className="space-y-2 text-xs">
<div className="p-2 rounded bg-purple-500/10 border border-purple-500/20">
<p className="text-purple-400 font-medium">
Заявки на партнёрство
</p>
<p className="text-white/60">
Отправка и получение заявок
</p>
</div>
<div className="p-2 rounded bg-purple-500/10 border border-purple-500/20">
<p className="text-purple-400 font-medium">
Управление контрагентами
</p>
<p className="text-white/60">
Ведение базы партнёров
</p>
</div>
</div>
</div>
<div className="p-4 rounded-lg bg-gray-500/10 border border-gray-500/20 text-center">
<Settings className="h-8 w-8 text-orange-400 mx-auto mb-2" />
<p className="text-white text-sm font-medium mb-2">
Настройки
</p>
<p className="text-white/60 text-xs">
Профиль и конфигурация
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Статистика участников */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white text-lg">
Участники процесса
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{actors.map((actor) => (
<div key={actor.name} className="text-center">
<Badge
className={`${actor.color} border px-3 py-2 text-xs font-medium`}
>
{actor.name}
</Badge>
<p className="text-white/50 text-xs mt-1">
{actor.count} шагов
</p>
</div>
))}
</div>
</CardContent>
</Card>
{/* Визуализация процесса */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white text-lg">
Схема бизнес-процесса
</CardTitle>
<p className="text-white/70 text-sm">
Полный цикл поставки товара от заказа до выставления счёта
</p>
</CardHeader>
<CardContent>
<div className="space-y-4">
{processSteps.map((step, index) => {
const IconComponent = step.icon;
const isLast = index === processSteps.length - 1;
return (
<div key={step.id} className="relative">
<div className="flex items-start gap-4 p-4 rounded-lg bg-white/5 backdrop-blur border border-white/10 hover:bg-white/10 transition-all duration-200">
{/* Иконка и номер */}
<div className="flex-shrink-0 relative">
<div className="w-12 h-12 rounded-full bg-white/10 flex items-center justify-center border border-white/20">
<IconComponent className="h-5 w-5 text-white" />
</div>
<div className="absolute -top-1 -right-1 w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center text-xs font-bold text-white">
{step.id}
</div>
</div>
{/* Контент */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4 mb-2">
<h4 className="text-white font-medium text-sm leading-tight">
{step.title}
</h4>
<Badge
className={`${getStatusColor(
step.status
)} border text-xs flex-shrink-0`}
>
{getStatusText(step.status)}
</Badge>
</div>
<p className="text-white/70 text-xs mb-3 leading-relaxed">
{step.description}
</p>
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-1">
<Users className="h-3 w-3 text-white/50" />
<span className="text-white/50">{step.actor}</span>
</div>
<div className="flex items-center gap-1">
<MapPin className="h-3 w-3 text-white/50" />
<span className="text-white/50">{step.location}</span>
</div>
</div>
</div>
</div>
{/* Стрелка к следующему шагу */}
{!isLast && (
<div className="flex justify-center py-2">
<ArrowRight className="h-4 w-4 text-white/30" />
</div>
)}
</div>
);
})}
</div>
</CardContent>
</Card>
{/* Легенда статусов */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white text-lg">Легенда статусов</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Badge className="bg-green-500/20 text-green-400 border-green-500/30 border">
Выполнено
</Badge>
<span className="text-white/70 text-sm">Этап завершён</span>
</div>
<div className="flex items-center gap-2">
<Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30 border">
В процессе
</Badge>
<span className="text-white/70 text-sm">Выполняется сейчас</span>
</div>
<div className="flex items-center gap-2">
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30 border">
Ожидает
</Badge>
<span className="text-white/70 text-sm">Ожидает выполнения</span>
</div>
</div>
</CardContent>
</Card>
{/* Диаграмма процесса */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white text-lg">
Диаграмма бизнес-процесса
</CardTitle>
<p className="text-white/70 text-sm">
Схематическое представление всех этапов процесса поставки
</p>
</CardHeader>
<CardContent>
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
<p className="text-white/70 text-sm mb-4 text-center">
Полная схема процесса от создания заказа до получения счёта
</p>
<div className="text-xs text-white/50 space-y-1">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-blue-500"></div>
<span>Селлер - создание и отслеживание заказа</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-green-500"></div>
<span>Поставщик - одобрение заявки</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-purple-500"></div>
<span>Фулфилмент - обработка и хранение</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-orange-500"></div>
<span>Логистика - доставка товара</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-gray-500"></div>
<span>Система - автоматические операции</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Схема взаимодействия систем */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white text-lg">
Схема взаимодействия систем
</CardTitle>
<p className="text-white/70 text-sm">
Упрощённая схема движения данных между кабинетами
</p>
</CardHeader>
<CardContent>
<div className="bg-white/5 rounded-lg p-6 border border-white/10">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Селлер */}
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-blue-500/20 flex items-center justify-center border-2 border-blue-500/30">
<ShoppingCart className="h-8 w-8 text-blue-400" />
</div>
<h4 className="text-blue-400 font-medium mb-2">Селлер</h4>
<div className="text-xs text-white/70 space-y-1">
<div> Создаёт заказ</div>
<div> Отслеживает статус</div>
<div> Получает счёт</div>
</div>
</div>
{/* Поставщик */}
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-green-500/20 flex items-center justify-center border-2 border-green-500/30">
<Users className="h-8 w-8 text-green-400" />
</div>
<h4 className="text-green-400 font-medium mb-2">Поставщик</h4>
<div className="text-xs text-white/70 space-y-1">
<div> Получает заявку</div>
<div> Одобряет поставку</div>
</div>
</div>
{/* Фулфилмент */}
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-purple-500/20 flex items-center justify-center border-2 border-purple-500/30">
<Warehouse className="h-8 w-8 text-purple-400" />
</div>
<h4 className="text-purple-400 font-medium mb-2">Фулфилмент</h4>
<div className="text-xs text-white/70 space-y-1">
<div> Принимает товар</div>
<div> Контролирует качество</div>
<div> Управляет складом</div>
</div>
</div>
{/* Логистика */}
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-orange-500/20 flex items-center justify-center border-2 border-orange-500/30">
<Truck className="h-8 w-8 text-orange-400" />
</div>
<h4 className="text-orange-400 font-medium mb-2">Логистика</h4>
<div className="text-xs text-white/70 space-y-1">
<div> Подтверждает заявку</div>
<div> Доставляет товар</div>
</div>
</div>
</div>
{/* Стрелки взаимодействия */}
<div className="mt-8 flex justify-center">
<div className="flex items-center gap-2 text-white/50 text-xs">
<ArrowRight className="h-4 w-4" />
<span>Передача данных</span>
<ArrowRight className="h-4 w-4" />
<span>Смена статуса</span>
<ArrowRight className="h-4 w-4" />
<span>Уведомления</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Ключевые точки интеграции */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white text-lg">
Ключевые точки интеграции
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
<h4 className="text-blue-400 font-medium mb-2 flex items-center gap-2">
<ShoppingCart className="h-4 w-4" />
Кабинет селлера
</h4>
<p className="text-white/70 text-sm">
Создание заказа, отслеживание статуса, получение счёта
</p>
</div>
<div className="p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<h4 className="text-green-400 font-medium mb-2 flex items-center gap-2">
<Users className="h-4 w-4" />
Кабинет поставщика
</h4>
<p className="text-white/70 text-sm">
Получение и одобрение заявок на поставку
</p>
</div>
<div className="p-4 rounded-lg bg-purple-500/10 border border-purple-500/20">
<h4 className="text-purple-400 font-medium mb-2 flex items-center gap-2">
<Warehouse className="h-4 w-4" />
Кабинет фулфилмента
</h4>
<p className="text-white/70 text-sm">
Управление поставками, приёмка, подготовка, контроль качества
</p>
</div>
<div className="p-4 rounded-lg bg-orange-500/10 border border-orange-500/20">
<h4 className="text-orange-400 font-medium mb-2 flex items-center gap-2">
<Truck className="h-4 w-4" />
Кабинет логистики
</h4>
<p className="text-white/70 text-sm">
Подтверждение заявок и организация доставки
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -19,7 +19,7 @@ export function SuppliesNavigationDemo() {
Навигация поставок
</h2>
<p className="text-white/70 mb-6">
Компоненты навигации, используемые в разделе "Мои поставки"
Компоненты навигации, используемые в разделе &quot;Мои поставки&quot;
</p>
</div>

View File

@ -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 (
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold animate-pulse">
{pendingOrders.length > 99 ? "99+" : pendingOrders.length}
</div>
);
}
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") ||
@ -521,7 +553,7 @@ export function Sidebar() {
</Button>
)}
{/* Отгрузки - для оптовиков */}
{/* Заявки - для оптовиков */}
{user?.organization?.type === "WHOLESALE" && (
<Button
variant={isSuppliesActive ? "secondary" : "ghost"}
@ -531,12 +563,16 @@ export function Sidebar() {
isSuppliesActive
? "bg-white/20 text-white hover:bg-white/30"
: "text-white/80 hover:bg-white/10 hover:text-white"
} cursor-pointer`}
} cursor-pointer relative`}
onClick={handleSuppliesClick}
title={isCollapsed ? "Отгрузки" : ""}
title={isCollapsed ? "Заявки" : ""}
>
<Truck className="h-4 w-4 flex-shrink-0" />
{!isCollapsed && <span className="ml-3">Отгрузки</span>}
{!isCollapsed && <span className="ml-3">Заявки</span>}
{/* Уведомление о новых заявках */}
{user?.organization?.type === "WHOLESALE" && (
<NewOrdersNotification />
)}
</Button>
)}

View File

@ -1,26 +1,52 @@
"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 {
interface SupplyOrder {
id: string;
partnerId: string;
deliveryDate: string;
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;
partner: {
id: string;
inn: string;
name: string;
fullName: string;
address?: string;
phones?: string[];
emails?: string[];
};
items: Array<{
id: string;
quantity: number;
price: number;
@ -30,66 +56,45 @@ interface SupplyOrderItem {
name: string;
article: string;
description?: string;
price: number;
quantity: number;
images?: string[];
mainImage?: string;
category?: {
id: string;
name: string;
};
};
}
interface SupplyOrder {
id: string;
organizationId: string;
deliveryDate: string;
status: string;
totalAmount: number;
totalItems: number;
fulfillmentCenterId?: string;
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;
type: string;
};
items: SupplyOrderItem[];
}>;
}
export function FulfillmentConsumablesOrdersTab() {
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(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<string, { label: string; color: string }> = {
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",
const { label, color, icon: Icon } = statusMap[status];
return (
<Badge className={`${color} border flex items-center gap-1 text-xs`}>
<Icon className="h-3 w-3" />
{label}
</Badge>
);
};
return <Badge className={`${color} border`}>{label}</Badge>;
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 formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
}).format(amount);
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 (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<span className="ml-3 text-white/60">Загрузка заказов расходников...</span>
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загрузка заказов поставок...</div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-red-400 font-medium">Ошибка загрузки заказов</p>
<p className="text-white/60 text-sm mt-2">{error.message}</p>
</div>
<div className="flex items-center justify-center p-8">
<div className="text-red-400">Ошибка загрузки заказов поставок</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Статистика входящих заказов расходников */}
<StatsGrid>
<StatsCard
title="Входящие заказы"
value={totalOrders}
icon={Package2}
iconColor="text-orange-400"
iconBg="bg-orange-500/20"
subtitle="Заказы от селлеров"
/>
<div className="space-y-2">
{/* Компактная статистика */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1 bg-blue-500/20 rounded">
<Clock className="h-3 w-3 text-blue-400" />
</div>
<div>
<p className="text-white/60 text-xs">Ожидание</p>
<p className="text-sm font-bold text-white">
{
fulfillmentOrders.filter(
(order) => order.status === "PENDING"
).length
}
</p>
</div>
</div>
</Card>
<StatsCard
title="Общая сумма"
value={formatCurrency(totalAmount)}
icon={TrendingUp}
iconColor="text-green-400"
iconBg="bg-green-500/20"
subtitle="Стоимость заказов"
/>
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1 bg-green-500/20 rounded">
<CheckCircle className="h-3 w-3 text-green-400" />
</div>
<div>
<p className="text-white/60 text-xs">Подтверждено</p>
<p className="text-sm font-bold text-white">
{
fulfillmentOrders.filter(
(order) => order.status === "CONFIRMED"
).length
}
</p>
</div>
</div>
</Card>
<StatsCard
title="Всего единиц"
value={totalItems}
icon={Wrench}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
subtitle="Количество расходников"
/>
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1 bg-yellow-500/20 rounded">
<Truck className="h-3 w-3 text-yellow-400" />
</div>
<div>
<p className="text-white/60 text-xs">В пути</p>
<p className="text-sm font-bold text-white">
{
fulfillmentOrders.filter(
(order) => order.status === "IN_TRANSIT"
).length
}
</p>
</div>
</div>
</Card>
<StatsCard
title="Новые заказы"
value={newOrders}
icon={Calendar}
iconColor="text-purple-400"
iconBg="bg-purple-500/20"
subtitle="Требуют обработки"
/>
</StatsGrid>
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1 bg-purple-500/20 rounded">
<Package className="h-3 w-3 text-purple-400" />
</div>
<div>
<p className="text-white/60 text-xs">Доставлено</p>
<p className="text-sm font-bold text-white">
{
fulfillmentOrders.filter(
(order) => order.status === "DELIVERED"
).length
}
</p>
</div>
</div>
</Card>
</div>
{/* Список входящих заказов расходников */}
{incomingSupplyOrders.length === 0 ? (
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
{/* Оптимизированный список заказов поставок */}
<div className="space-y-1.5">
{ordersWithNumbers.length === 0 ? (
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="text-center">
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">
Пока нет заказов расходников
<Package className="h-8 w-8 text-white/40 mx-auto mb-2" />
<h3 className="text-sm font-semibold text-white mb-1">
Нет заказов поставок
</h3>
<p className="text-white/60">
Здесь будут отображаться заказы расходников от селлеров
<p className="text-white/60 text-xs">
Заказы поставок расходников будут отображаться здесь
</p>
</div>
</Card>
) : (
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-4 text-white font-semibold">ID</th>
<th className="text-left p-4 text-white font-semibold">
Селлер
</th>
<th className="text-left p-4 text-white font-semibold">
Поставщик
</th>
<th className="text-left p-4 text-white font-semibold">
Дата поставки
</th>
<th className="text-left p-4 text-white font-semibold">
Дата заказа
</th>
<th className="text-left p-4 text-white font-semibold">
Количество
</th>
<th className="text-left p-4 text-white font-semibold">
Сумма
</th>
<th className="text-left p-4 text-white font-semibold">
Статус
</th>
</tr>
</thead>
<tbody>
{incomingSupplyOrders.map((order) => {
const isOrderExpanded = expandedOrders.has(order.id);
return (
<React.Fragment key={order.id}>
{/* Основная строка заказа */}
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer"
ordersWithNumbers.map((order) => (
<Card
key={order.id}
className="bg-white/10 backdrop-blur border-white/20 overflow-hidden hover:bg-white/15 transition-colors cursor-pointer"
onClick={() => toggleOrderExpansion(order.id)}
>
<td className="p-4">
<div className="flex items-center space-x-2">
{isOrderExpanded ? (
<ChevronDown className="h-4 w-4 text-white/60" />
) : (
<ChevronRight className="h-4 w-4 text-white/60" />
{/* Компактная основная информация */}
<div className="px-3 py-2">
<div className="flex items-center justify-between">
{/* Левая часть - основная информация */}
<div className="flex items-center space-x-3 flex-1 min-w-0">
{/* Номер поставки */}
<div className="flex items-center space-x-1">
<Hash className="h-3 w-3 text-white/60 flex-shrink-0" />
<span className="text-white font-semibold text-sm">
{order.number}
</span>
</div>
{/* Селлер */}
<div className="flex items-center space-x-2 min-w-0">
<div className="flex flex-col items-center">
<Store className="h-3 w-3 text-blue-400 mb-0.5" />
<span className="text-blue-400 text-xs font-medium leading-none">
Селлер
</span>
</div>
<div className="flex items-center space-x-1.5">
<Avatar className="w-7 h-7 flex-shrink-0">
<AvatarFallback className="bg-blue-500 text-white text-xs">
{getInitials(
order.partner.name || order.partner.fullName
)}
<span className="text-white font-medium">
{order.id.slice(-8)}
</span>
</div>
</td>
<td className="p-4">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<User className="h-4 w-4 text-white/40" />
<span className="text-white font-medium">
{order.organization.name || order.organization.fullName || "Селлер"}
</span>
</div>
<p className="text-white/60 text-sm">
Тип: {order.organization.type}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<h3 className="text-white font-medium text-sm truncate max-w-[120px]">
{order.partner.name || order.partner.fullName}
</h3>
<p className="text-white/60 text-xs">
{order.partner.inn}
</p>
</div>
</td>
<td className="p-4">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Building2 className="h-4 w-4 text-white/40" />
<span className="text-white font-medium">
{order.partner.name || order.partner.fullName || "Поставщик"}
</div>
</div>
{/* Поставщик (фулфилмент-центр) */}
<div className="hidden xl:flex items-center space-x-2 min-w-0">
<div className="flex flex-col items-center">
<Building className="h-3 w-3 text-green-400 mb-0.5" />
<span className="text-green-400 text-xs font-medium leading-none">
Поставщик
</span>
</div>
<p className="text-white/60 text-sm">
ИНН: {order.partner.inn}
</p>
<div className="flex items-center space-x-1.5">
<Avatar className="w-7 h-7 flex-shrink-0">
<AvatarFallback className="bg-green-500 text-white text-xs">
{getInitials(user?.organization?.name || "ФФ")}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<h3 className="text-white font-medium text-sm truncate max-w-[100px]">
{user?.organization?.name || "ФФ-центр"}
</h3>
<p className="text-white/60 text-xs">Наш ФФ</p>
</div>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-white/40" />
<span className="text-white font-semibold">
</div>
</div>
{/* Краткие данные */}
<div className="hidden lg:flex items-center space-x-4">
<div className="flex items-center space-x-1">
<Calendar className="h-3 w-3 text-blue-400" />
<span className="text-white text-xs">
{formatDate(order.deliveryDate)}
</span>
</div>
</td>
<td className="p-4">
<span className="text-white/80">
{formatDateTime(order.createdAt)}
<div className="flex items-center space-x-1">
<Package className="h-3 w-3 text-green-400" />
<span className="text-white text-xs">
{order.totalItems}
</span>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{order.totalItems} шт
</div>
<div className="flex items-center space-x-1">
<Layers className="h-3 w-3 text-purple-400" />
<span className="text-white text-xs">
{order.items.length}
</span>
</td>
<td className="p-4">
</div>
</div>
</div>
{/* Правая часть - статус и действия */}
<div className="flex items-center space-x-2 flex-shrink-0">
<Badge
className={`${
order.status === "PENDING"
? "bg-blue-500/20 text-blue-300 border-blue-500/30"
: order.status === "CONFIRMED"
? "bg-green-500/20 text-green-300 border-green-500/30"
: order.status === "IN_TRANSIT"
? "bg-yellow-500/20 text-yellow-300 border-yellow-500/30"
: order.status === "DELIVERED"
? "bg-purple-500/20 text-purple-300 border-purple-500/30"
: "bg-red-500/20 text-red-300 border-red-500/30"
} border flex items-center gap-1 text-xs px-2 py-1`}
>
{order.status === "PENDING" && (
<Clock className="h-3 w-3" />
)}
{order.status === "CONFIRMED" && (
<CheckCircle className="h-3 w-3" />
)}
{order.status === "IN_TRANSIT" && (
<Truck className="h-3 w-3" />
)}
{order.status === "DELIVERED" && (
<Package className="h-3 w-3" />
)}
{order.status === "CANCELLED" && (
<XCircle className="h-3 w-3" />
)}
{order.status === "PENDING" && "Ожидание"}
{order.status === "CONFIRMED" && "Подтверждена"}
{order.status === "IN_TRANSIT" && "В пути"}
{order.status === "DELIVERED" && "Доставлена"}
{order.status === "CANCELLED" && "Отменена"}
</Badge>
{canMarkAsDelivered(order.status) && (
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
handleStatusUpdate(order.id, "DELIVERED");
}}
disabled={updating}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-2 py-1 h-7"
>
<CheckCircle className="h-3 w-3 mr-1" />
Получено
</Button>
)}
</div>
</div>
{/* Мобильная версия поставщика и кратких данных */}
<div className="xl:hidden mt-2 space-y-1">
{/* Поставщик на мобильных */}
<div className="flex items-center space-x-2">
<DollarSign className="h-4 w-4 text-white/40" />
<span className="text-green-400 font-bold">
<div className="flex flex-col items-center">
<Building className="h-3 w-3 text-green-400 mb-0.5" />
<span className="text-green-400 text-xs font-medium leading-none">
Поставщик
</span>
</div>
<div className="flex items-center space-x-1.5">
<Avatar className="w-6 h-6 flex-shrink-0">
<AvatarFallback className="bg-green-500 text-white text-xs">
{getInitials(user?.organization?.name || "ФФ")}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<h3 className="text-white font-medium text-sm truncate">
{user?.organization?.name || "Фулфилмент-центр"}
</h3>
</div>
</div>
</div>
{/* Краткие данные на мобильных */}
<div className="lg:hidden flex items-center justify-between text-xs">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-1">
<Calendar className="h-3 w-3 text-blue-400" />
<span className="text-white">
{formatDate(order.deliveryDate)}
</span>
</div>
<div className="flex items-center space-x-1">
<Package className="h-3 w-3 text-green-400" />
<span className="text-white">
{order.totalItems} шт.
</span>
</div>
<div className="flex items-center space-x-1">
<Layers className="h-3 w-3 text-purple-400" />
<span className="text-white">
{order.items.length} поз.
</span>
</div>
</div>
</div>
</div>
{/* Развернутые детали заказа */}
{expandedOrders.has(order.id) && (
<>
<Separator className="my-2 bg-white/10" />
{/* Сумма заказа */}
<div className="mb-2 p-2 bg-white/5 rounded">
<div className="flex items-center justify-between">
<span className="text-white/60 text-sm">
Общая сумма:
</span>
<span className="text-white font-semibold text-base">
{formatCurrency(order.totalAmount)}
</span>
</div>
</td>
<td className="p-4">{getStatusBadge(order.status)}</td>
</tr>
</div>
{/* Развернутая информация о заказе */}
{isOrderExpanded && (
<tr>
<td colSpan={8} className="p-0">
<div className="bg-white/5 border-t border-white/10">
<div className="p-6">
<h4 className="text-white font-semibold mb-4">
Состав заказа от селлера:
{/* Информация о поставщике */}
<div className="mb-3">
<h4 className="text-white font-semibold mb-1.5 flex items-center text-sm">
<Building className="h-4 w-4 mr-1.5 text-blue-400" />
Информация о селлере
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{order.items.map((item) => (
<Card
key={item.id}
className="bg-white/10 backdrop-blur border-white/20 p-4"
>
<div className="space-y-3">
<div className="bg-white/5 rounded p-2 space-y-1.5">
{order.partner.address && (
<div className="flex items-center space-x-2">
<MapPin className="h-3 w-3 text-white/60 flex-shrink-0" />
<span className="text-white/80 text-sm">
{order.partner.address}
</span>
</div>
)}
{order.partner.phones &&
order.partner.phones.length > 0 && (
<div className="flex items-center space-x-2">
<Phone className="h-3 w-3 text-white/60 flex-shrink-0" />
<span className="text-white/80 text-sm">
{order.partner.phones.join(", ")}
</span>
</div>
)}
{order.partner.emails &&
order.partner.emails.length > 0 && (
<div className="flex items-center space-x-2">
<Mail className="h-3 w-3 text-white/60 flex-shrink-0" />
<span className="text-white/80 text-sm">
{order.partner.emails.join(", ")}
</span>
</div>
)}
</div>
</div>
{/* Список товаров */}
<div>
<h5 className="text-white font-medium mb-1">
<h4 className="text-white font-semibold mb-1.5 flex items-center text-sm">
<Package className="h-4 w-4 mr-1.5 text-green-400" />
Товары ({order.items.length})
</h4>
<div className="space-y-1.5">
{order.items.map((item) => (
<div
key={item.id}
className="bg-white/5 rounded p-2 flex items-center justify-between"
>
<div className="flex items-center space-x-2 flex-1 min-w-0">
{item.product.mainImage && (
<img
src={item.product.mainImage}
alt={item.product.name}
className="w-8 h-8 rounded object-cover flex-shrink-0"
/>
)}
<div className="min-w-0 flex-1">
<h5 className="text-white font-medium text-sm truncate">
{item.product.name}
</h5>
<p className="text-white/60 text-sm">
Артикул: {item.product.article}
<p className="text-white/60 text-xs">
{item.product.article}
</p>
{item.product.category && (
<Badge className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs mt-2">
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs mt-0.5 px-1.5 py-0.5"
>
{item.product.category.name}
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<p className="text-white/60">
Количество: {item.quantity} шт
</p>
<p className="text-white/60">
Цена: {formatCurrency(item.price)}
</p>
</div>
<div className="text-right">
<p className="text-green-400 font-semibold">
<div className="text-right flex-shrink-0">
<p className="text-white font-semibold text-sm">
{item.quantity} шт.
</p>
<p className="text-white/60 text-xs">
{formatCurrency(item.price)}
</p>
<p className="text-green-400 font-semibold text-sm">
{formatCurrency(item.totalPrice)}
</p>
</div>
</div>
</div>
</Card>
))}
</div>
</div>
</div>
</td>
</tr>
</>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
</Card>
))
)}
</div>
</div>
);
}

View File

@ -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<keyof StoreData>("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set());
const [expandedItems, setExpandedItems] = useState<Set<string>>(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 (
<div
@ -314,8 +638,8 @@ export function FulfillmentWarehouseDashboard() {
sortable?: boolean;
}) => (
<div
className={`px-3 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider ${
sortable ? "cursor-pointer hover:text-white hover:bg-white/5" : ""
className={`px-3 py-2 text-left text-xs font-medium text-blue-100 uppercase tracking-wider ${
sortable ? "cursor-pointer hover:text-white hover:bg-blue-500/10" : ""
} flex items-center space-x-1`}
onClick={sortable && field ? () => handleSort(field) : undefined}
>
@ -350,6 +674,45 @@ export function FulfillmentWarehouseDashboard() {
</div>
);
// Индикатор загрузки
if (counterpartiesLoading || ordersLoading) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex items-center justify-center`}
>
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<span className="text-white/60">Загрузка данных склада...</span>
</div>
</main>
</div>
);
}
// Индикатор ошибки
if (counterpartiesError || ordersError) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex items-center justify-center`}
>
<div className="text-center">
<AlertTriangle className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-red-400 font-medium">
Ошибка загрузки данных склада
</p>
<p className="text-white/60 text-sm mt-2">
{counterpartiesError?.message || ordersError?.message}
</p>
</div>
</main>
</div>
);
}
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
@ -359,9 +722,42 @@ export function FulfillmentWarehouseDashboard() {
{/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
<div className="glass-card p-4">
<h2 className="text-base font-semibold text-blue-400 mb-3">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold text-blue-400">
Статистика склада
</h2>
{/* Индикатор обновления данных */}
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 text-xs text-white/60">
<Clock className="h-3 w-3" />
<span>Обновлено из поставок</span>
{supplyOrders.filter((o) => o.status === "DELIVERED").length >
0 && (
<Badge className="bg-green-500/20 text-green-300 border-green-500/30 text-xs">
{
supplyOrders.filter((o) => o.status === "DELIVERED")
.length
}{" "}
поставок получено
</Badge>
)}
</div>
<Button
variant="outline"
size="sm"
className="h-7 text-xs bg-white/10 border-white/20 text-white hover:bg-white/20"
onClick={() => {
refetchCounterparties();
refetchOrders();
toast.success("Данные склада обновлены");
}}
disabled={counterpartiesLoading || ordersLoading}
>
<RotateCcw className="h-3 w-3 mr-1" />
Обновить
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
<StatCard
title="Продукты"
@ -421,8 +817,24 @@ export function FulfillmentWarehouseDashboard() {
style={{ maxHeight: "10vh" }}
>
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-white">
Детализация по магазинам
<h2 className="text-base font-semibold text-white flex items-center space-x-2">
<Store className="h-4 w-4 text-blue-400" />
<span>Детализация по партнерам-селлерам</span>
<div className="flex items-center space-x-1 text-xs text-white/60">
<div className="flex items-center space-x-1">
<div className="flex space-x-0.5">
<div className="w-2 h-2 bg-blue-400 rounded"></div>
<div className="w-2 h-2 bg-pink-400 rounded"></div>
<div className="w-2 h-2 bg-emerald-400 rounded"></div>
</div>
<span>Селлеры</span>
</div>
<ChevronRight className="h-3 w-3" />
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded"></div>
<span>Товары</span>
</div>
</div>
</h2>
{/* Компактный поиск */}
@ -430,7 +842,7 @@ export function FulfillmentWarehouseDashboard() {
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
<div className="flex space-x-2">
<Input
placeholder="Поиск по магазинам..."
placeholder="Поиск по селлерам..."
value={searchTerm}
onChange={(e) => 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} селлеров
</Badge>
</div>
</div>
{/* Фиксированные заголовки таблицы */}
<div className="flex-shrink-0 bg-white/5 border-b border-white/10">
{/* Фиксированные заголовки таблицы - Уровень 1 (Поставщики) */}
<div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
<div className="grid grid-cols-6 gap-0">
<TableHeader field="name" sortable>
/ Магазин
/ Селлер
</TableHeader>
<TableHeader field="products" sortable>
Продукты
@ -477,8 +889,8 @@ export function FulfillmentWarehouseDashboard() {
</div>
</div>
{/* Строка с суммами */}
<div className="flex-shrink-0 bg-blue-500/10 border-b border-blue-500/20">
{/* Строка с суммами - Уровень 1 (Поставщики) */}
<div className="flex-shrink-0 bg-blue-500/25 border-b border-blue-500/50">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-2 text-xs font-bold text-blue-300">
ИТОГО ({filteredAndSortedStores.length})
@ -499,29 +911,28 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400"
}`}
>
{(
{totals.products > 0
? (
(totals.productsChange / totals.products) *
100
).toFixed(1)}
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.productsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.productsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{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"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.goodsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.goodsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.goodsChange)}
@ -590,29 +1002,28 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400"
}`}
>
{(
{totals.defects > 0
? (
(totals.defectsChange / totals.defects) *
100
).toFixed(1)}
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.defectsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.defectsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.defectsChange)}
@ -637,18 +1048,19 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400"
}`}
>
{(
{totals.sellerSupplies > 0
? (
(totals.sellerSuppliesChange /
totals.sellerSupplies) *
100
).toFixed(1)}
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
@ -657,7 +1069,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
@ -666,7 +1077,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.sellerSuppliesChange)}
@ -691,29 +1101,28 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400"
}`}
>
{(
{totals.pvzReturns > 0
? (
(totals.pvzReturnsChange / totals.pvzReturns) *
100
).toFixed(1)}
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.pvzReturnsChange)}
@ -727,12 +1136,33 @@ export function FulfillmentWarehouseDashboard() {
{/* Скроллируемый контент таблицы - оставшееся пространство */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
{filteredAndSortedStores.map((store, index) => (
{filteredAndSortedStores.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
<p className="text-white/60 font-medium">
{sellerPartners.length === 0
? "Нет партнеров-селлеров"
: "Партнеры не найдены"}
</p>
<p className="text-white/40 text-sm mt-2">
{sellerPartners.length === 0
? "Добавьте партнеров-селлеров для отображения данных склада"
: searchTerm
? "Попробуйте изменить поисковый запрос"
: "Данные о партнерах-селлерах будут отображены здесь"}
</p>
</div>
</div>
) : (
filteredAndSortedStores.map((store, index) => {
const colorScheme = getColorScheme(store.id);
return (
<div
key={store.id}
className="border-b border-white/10 hover:bg-white/5 transition-colors"
className={`border-b ${colorScheme.border} ${colorScheme.hover} transition-colors border-l-8 ${colorScheme.borderLeft} ${colorScheme.bg} shadow-sm hover:shadow-md`}
>
{/* Основная строка магазина */}
{/* Основная строка поставщика */}
<div
className="grid grid-cols-6 gap-0 cursor-pointer"
onClick={() => toggleStoreExpansion(store.id)}
@ -742,12 +1172,29 @@ export function FulfillmentWarehouseDashboard() {
{filteredAndSortedStores.length - index}
</span>
<div className="flex items-center space-x-2">
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-500 rounded-md flex items-center justify-center">
<Store className="h-3 w-3 text-white" />
</div>
<Avatar className="w-6 h-6">
{store.avatar && (
<AvatarImage
src={store.avatar}
alt={store.name}
/>
)}
<AvatarFallback
className={`${getColorForStore(
store.id
)} text-white font-medium text-xs`}
>
{getInitials(store.name)}
</AvatarFallback>
</Avatar>
<div>
<div className="text-white font-medium text-xs">
<div className="text-white font-medium text-xs flex items-center space-x-2">
<div
className={`w-3 h-3 ${colorScheme.indicator} rounded flex-shrink-0 border`}
></div>
<span className={colorScheme.text}>
{store.name}
</span>
</div>
</div>
</div>
@ -755,12 +1202,13 @@ export function FulfillmentWarehouseDashboard() {
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.products)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
@ -769,7 +1217,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
@ -778,7 +1225,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.productsChange)}
@ -791,24 +1237,29 @@ export function FulfillmentWarehouseDashboard() {
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.goods)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(store.goodsChange * 0.6))}
+
{Math.abs(
Math.floor(store.goodsChange * 0.6)
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(store.goodsChange * 0.4))}
-
{Math.abs(
Math.floor(store.goodsChange * 0.4)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.goodsChange)}
@ -821,12 +1272,13 @@ export function FulfillmentWarehouseDashboard() {
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.defects)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
@ -835,7 +1287,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
@ -844,7 +1295,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.defectsChange)}
@ -857,30 +1307,33 @@ export function FulfillmentWarehouseDashboard() {
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.sellerSupplies)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
{Math.abs(
Math.floor(store.sellerSuppliesChange * 0.6)
Math.floor(
store.sellerSuppliesChange * 0.6
)
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
{Math.abs(
Math.floor(store.sellerSuppliesChange * 0.4)
Math.floor(
store.sellerSuppliesChange * 0.4
)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.sellerSuppliesChange)}
@ -893,12 +1346,13 @@ export function FulfillmentWarehouseDashboard() {
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold text-sm">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.pvzReturns)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+
@ -907,7 +1361,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-
@ -916,7 +1369,6 @@ export function FulfillmentWarehouseDashboard() {
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.pvzReturnsChange)}
@ -928,69 +1380,278 @@ export function FulfillmentWarehouseDashboard() {
</div>
</div>
{/* Расширенная информация */}
{/* Второй уровень - детализация по товарам */}
{expandedStores.has(store.id) && (
<div className="bg-white/5 px-3 py-3 border-t border-white/10">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<Package2 className="h-3 w-3 text-blue-400" />
<span className="text-blue-300 text-xs font-medium">
Продукты
</span>
<div className="bg-green-500/5 border-t border-green-500/20">
{/* Статическая часть - заголовки столбцов второго уровня */}
<div className="border-b border-green-500/20 bg-green-500/10">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider">
Наименование
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.products)}
</div>
<div className="text-blue-200/60 text-[10px]">
Готовые к отправке
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<Package className="h-3 w-3 text-cyan-400" />
<span className="text-cyan-300 text-xs font-medium">
Товары
</span>
{/* Динамическая часть - данные по товарам (скроллируемая) */}
<div className="max-h-64 overflow-y-auto scrollbar-thin scrollbar-thumb-green-500/30 scrollbar-track-transparent">
{store.items?.map((item) => (
<div key={item.id}>
{/* Основная строка товара */}
<div
className="border-b border-green-500/15 hover:bg-green-500/10 transition-colors cursor-pointer border-l-4 border-l-green-500/40 ml-4"
onClick={() => toggleItemExpansion(item.id)}
>
<div className="grid grid-cols-6 gap-0">
{/* Наименование */}
<div className="px-3 py-2 flex items-center">
<div className="flex-1">
<div className="text-white font-medium text-xs flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded flex-shrink-0"></div>
<span>{item.name}</span>
{item.variants &&
item.variants.length > 0 && (
<Badge
variant="secondary"
className="bg-orange-500/20 text-orange-300 text-[9px] px-1 py-0"
>
{item.variants.length} вар.
</Badge>
)}
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.goods)}
<div className="text-white/60 text-[10px]">
{item.article}
</div>
<div className="text-cyan-200/60 text-[10px]">
В обработке
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<AlertTriangle className="h-3 w-3 text-red-400" />
<span className="text-red-300 text-xs font-medium">
Брак
</span>
{/* Продукты */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.productQuantity)}
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.defects)}
</div>
<div className="text-red-200/60 text-[10px]">
К утилизации
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.productPlace || "-"}
</div>
</div>
<div className="glass-secondary rounded-lg p-2">
<div className="flex items-center space-x-1.5 mb-1">
<RotateCcw className="h-3 w-3 text-yellow-400" />
<span className="text-yellow-300 text-xs font-medium">
Возвраты
</span>
{/* Товары */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.goodsQuantity)}
</div>
<div className="text-white text-sm font-bold">
{formatNumber(store.pvzReturns)}
</div>
<div className="text-yellow-200/60 text-[10px]">
С ПВЗ
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.goodsPlace || "-"}
</div>
</div>
{/* Брак */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.defectsQuantity)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.defectsPlace || "-"}
</div>
</div>
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(
item.sellerSuppliesQuantity
)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.sellerSuppliesPlace || "-"}
</div>
</div>
{/* Возвраты с ПВЗ */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.pvzReturnsQuantity)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.pvzReturnsPlace || "-"}
</div>
</div>
</div>
</div>
{/* Третий уровень - варианты товара */}
{expandedItems.has(item.id) &&
item.variants &&
item.variants.length > 0 && (
<div className="bg-orange-500/5 border-t border-orange-500/20">
{/* Заголовки для вариантов */}
<div className="border-b border-orange-500/20 bg-orange-500/10">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider">
Вариант
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
</div>
</div>
{/* Данные по вариантам */}
<div className="max-h-32 overflow-y-auto scrollbar-thin scrollbar-thumb-orange-500/30 scrollbar-track-transparent">
{item.variants.map((variant) => (
<div
key={variant.id}
className="border-b border-orange-500/15 hover:bg-orange-500/10 transition-colors border-l-4 border-l-orange-500/50 ml-8"
>
<div className="grid grid-cols-6 gap-0">
{/* Название варианта */}
<div className="px-3 py-1.5">
<div className="text-white font-medium text-[10px] flex items-center space-x-2">
<div className="w-1.5 h-1.5 bg-orange-500 rounded flex-shrink-0"></div>
<span>{variant.name}</span>
</div>
</div>
{/* Продукты */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.productQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.productPlace || "-"}
</div>
</div>
{/* Товары */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.goodsQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.goodsPlace || "-"}
</div>
</div>
{/* Брак */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.defectsQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.defectsPlace || "-"}
</div>
</div>
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.sellerSuppliesQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.sellerSuppliesPlace ||
"-"}
</div>
</div>
{/* Возвраты с ПВЗ */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.pvzReturnsQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.pvzReturnsPlace ||
"-"}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
@ -998,6 +1659,13 @@ export function FulfillmentWarehouseDashboard() {
))}
</div>
</div>
)}
</div>
);
})
)}
</div>
</div>
</div>
</main>
</div>

View File

@ -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<Set<string>>(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) => ({

View File

@ -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,10 +45,12 @@ export function CreateSupplyPage() {
const [goodsVolume, setGoodsVolume] = useState<number>(0);
const [cargoPlaces, setCargoPlaces] = useState<number>(0);
const [goodsPrice, setGoodsPrice] = useState<number>(0);
const [fulfillmentServicesPrice, setFulfillmentServicesPrice] = useState<number>(0);
const [fulfillmentServicesPrice, setFulfillmentServicesPrice] =
useState<number>(0);
const [logisticsPrice, setLogisticsPrice] = useState<number>(0);
const [selectedServicesCost, setSelectedServicesCost] = useState<number>(0);
const [selectedConsumablesCost, setSelectedConsumablesCost] = useState<number>(0);
const [selectedConsumablesCost, setSelectedConsumablesCost] =
useState<number>(0);
const [hasItemsInSupply, setHasItemsInSupply] = useState<boolean>(false);
// Загружаем контрагентов-фулфилментов
@ -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;
const selectedSupplier = suppliersData.find(
(supplier: unknown) => (supplier as { selected?: boolean }).selected
);
const supplierMarket = (selectedSupplier as { market?: string })?.market;
console.log("Обновление поставщиков:", { selectedSupplier, supplierMarket, volume: goodsVolume });
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,11 +157,20 @@ 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) {
@ -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() {
<Select
value={selectedFulfillment}
onValueChange={(value) => {
console.log('Выбран фулфилмент:', value);
console.log("Выбран фулфилмент:", value);
setSelectedFulfillment(value);
}}
>
@ -282,7 +318,9 @@ export function CreateSupplyPage() {
</Label>
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
<span className="text-white/80 text-xs">
{goodsVolume > 0 ? `${goodsVolume.toFixed(2)} м³` : 'Рассчитывается автоматически'}
{goodsVolume > 0
? `${goodsVolume.toFixed(2)} м³`
: "Рассчитывается автоматически"}
</span>
</div>
</div>
@ -295,7 +333,9 @@ export function CreateSupplyPage() {
<Input
type="number"
value={cargoPlaces || ""}
onChange={(e) => 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() {
</Label>
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
<span className="text-white/80 text-xs font-medium">
{goodsPrice > 0 ? formatCurrency(goodsPrice) : 'Рассчитывается автоматически'}
{goodsPrice > 0
? formatCurrency(goodsPrice)
: "Рассчитывается автоматически"}
</span>
</div>
</div>
@ -323,7 +365,9 @@ export function CreateSupplyPage() {
</Label>
<div className="h-8 bg-green-500/20 border border-green-400/30 rounded-lg flex items-center px-3">
<span className="text-green-400 text-xs font-medium">
{selectedServicesCost > 0 ? formatCurrency(selectedServicesCost) : 'Выберите услуги'}
{selectedServicesCost > 0
? formatCurrency(selectedServicesCost)
: "Выберите услуги"}
</span>
</div>
</div>
@ -335,7 +379,9 @@ export function CreateSupplyPage() {
</Label>
<div className="h-8 bg-orange-500/20 border border-orange-400/30 rounded-lg flex items-center px-3">
<span className="text-orange-400 text-xs font-medium">
{selectedConsumablesCost > 0 ? formatCurrency(selectedConsumablesCost) : 'Выберите расходники'}
{selectedConsumablesCost > 0
? formatCurrency(selectedConsumablesCost)
: "Выберите расходники"}
</span>
</div>
</div>
@ -347,12 +393,12 @@ export function CreateSupplyPage() {
</Label>
<div className="h-8 bg-blue-500/20 border border-blue-400/30 rounded-lg flex items-center px-3">
<span className="text-blue-400 text-xs font-medium">
{logisticsPrice > 0 ? formatCurrency(logisticsPrice) : 'Выберите поставщика'}
{logisticsPrice > 0
? formatCurrency(logisticsPrice)
: "Выберите поставщика"}
</span>
</div>
</div>
</div>
{/* 9. Итоговая сумма */}
@ -370,11 +416,21 @@ export function CreateSupplyPage() {
{/* 10. Кнопка создания поставки */}
<Button
onClick={handleCreateSupplyClick}
disabled={!canCreateSupply || isCreatingSupply || !deliveryDate || !selectedFulfillment || !hasItemsInSupply}
disabled={
!canCreateSupply ||
isCreatingSupply ||
!deliveryDate ||
!selectedFulfillment ||
!hasItemsInSupply
}
className={`w-full h-12 text-sm font-medium transition-all duration-200 ${
canCreateSupply && deliveryDate && selectedFulfillment && hasItemsInSupply && !isCreatingSupply
? 'bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white'
: 'bg-gray-500/20 text-gray-400 cursor-not-allowed'
canCreateSupply &&
deliveryDate &&
selectedFulfillment &&
hasItemsInSupply &&
!isCreatingSupply
? "bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white"
: "bg-gray-500/20 text-gray-400 cursor-not-allowed"
}`}
>
{isCreatingSupply ? (
@ -383,7 +439,7 @@ export function CreateSupplyPage() {
<span>Создание...</span>
</div>
) : (
'Создать поставку'
"Создать поставку"
)}
</Button>
</Card>

View File

@ -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);
// Вызываем асинхронно чтобы не обновлять состояние во время рендера

View File

@ -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 (
<div className="h-full overflow-hidden">
<Tabs
@ -57,7 +63,7 @@ export function FulfillmentSuppliesTab({
</TabsContent>
<TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden">
<RealSupplyOrdersTab />
{isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
</TabsContent>
<TabsContent value="returns" className="mt-0 flex-1 overflow-hidden">

View File

@ -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<Set<string>>(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<string, { label: string; color: string }> = {
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",
};
const { label, color, icon: Icon } = statusMap[status];
return <Badge className={`${color} border`}>{label}</Badge>;
return (
<Badge className={`${color} border flex items-center gap-1`}>
<Icon className="h-3 w-3" />
{label}
</Badge>
);
};
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 (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<span className="ml-3 text-white/60">Загрузка заказов расходников...</span>
<span className="ml-3 text-white/60">Загрузка заявок...</span>
</div>
);
}
@ -155,7 +242,7 @@ export function RealSupplyOrdersTab() {
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-red-400 font-medium">Ошибка загрузки заказов</p>
<p className="text-red-400 font-medium">Ошибка загрузки заявок</p>
<p className="text-white/60 text-sm mt-2">{error.message}</p>
</div>
</div>
@ -163,16 +250,43 @@ export function RealSupplyOrdersTab() {
}
return (
<div className="h-full flex flex-col space-y-6">
{/* Статистика заказов расходников */}
<div className="space-y-6">
{/* Статистика входящих заявок */}
<StatsGrid>
<StatsCard
title="Всего заказов"
title="Всего заявок"
value={totalOrders}
icon={Package2}
iconColor="text-orange-400"
iconBg="bg-orange-500/20"
subtitle="Заказы расходников"
subtitle="Заявки от селлеров"
/>
<StatsCard
title="Ожидают одобрения"
value={pendingOrders}
icon={Clock}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
subtitle="Требуют решения"
/>
<StatsCard
title="Одобрено"
value={approvedOrders}
icon={CheckCircle}
iconColor="text-green-400"
iconBg="bg-green-500/20"
subtitle="Готовы к отправке"
/>
<StatsCard
title="В пути"
value={inTransitOrders}
icon={Truck}
iconColor="text-yellow-400"
iconBg="bg-yellow-500/20"
subtitle="Доставляются"
/>
<StatsCard
@ -181,56 +295,47 @@ export function RealSupplyOrdersTab() {
icon={TrendingUp}
iconColor="text-green-400"
iconBg="bg-green-500/20"
subtitle="Стоимость заказов"
subtitle="Стоимость заявок"
/>
<StatsCard
title="Всего единиц"
value={totalItems}
icon={Wrench}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
subtitle="Количество расходников"
/>
<StatsCard
title="Завершено"
value={completedOrders}
icon={Calendar}
iconColor="text-purple-400"
iconBg="bg-purple-500/20"
subtitle="Доставленные заказы"
subtitle="Количество товаров"
/>
</StatsGrid>
{/* Список заказов расходников */}
{supplyOrders.length === 0 ? (
{/* Список входящих заявок */}
{incomingSupplyOrders.length === 0 ? (
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
<div className="text-center">
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">
Пока нет заказов расходников
Пока нет заявок
</h3>
<p className="text-white/60">
Создайте первый заказ расходников через кнопку &quot;Создать поставку&quot;
Здесь будут отображаться заявки от селлеров на поставку товаров
</p>
</div>
</Card>
) : (
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden flex-1 flex flex-col">
<div className="overflow-auto flex-1">
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-4 text-white font-semibold">ID</th>
<th className="text-left p-4 text-white font-semibold">
Поставщик
Заказчик
</th>
<th className="text-left p-4 text-white font-semibold">
Дата поставки
</th>
<th className="text-left p-4 text-white font-semibold">
Дата создания
Дата заявки
</th>
<th className="text-left p-4 text-white font-semibold">
Количество
@ -241,34 +346,48 @@ export function RealSupplyOrdersTab() {
<th className="text-left p-4 text-white font-semibold">
Статус
</th>
<th className="text-left p-4 text-white font-semibold">
Действия
</th>
</tr>
</thead>
<tbody>
{supplyOrders.map((order) => {
{incomingSupplyOrders.map((order) => {
const isOrderExpanded = expandedOrders.has(order.id);
return (
<React.Fragment key={order.id}>
{/* Основная строка заказа */}
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer"
onClick={() => toggleOrderExpansion(order.id)}
>
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors">
<td className="p-4">
<div className="flex items-center space-x-2">
<button
onClick={() => toggleOrderExpansion(order.id)}
className="text-white/60 hover:text-white"
>
{isOrderExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
<span className="text-white font-medium">
{order.id.slice(-8)}
</span>
</div>
</td>
<td className="p-4">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Building2 className="h-4 w-4 text-white/40" />
<User className="h-4 w-4 text-white/40" />
<span className="text-white font-medium">
{order.partner.name || order.partner.fullName || "Поставщик"}
{order.organization.name ||
order.organization.fullName ||
"Заказчик"}
</span>
</div>
<p className="text-white/60 text-sm">
ИНН: {order.partner.inn}
Тип: {order.organization.type}
</p>
</div>
</td>
@ -299,16 +418,74 @@ export function RealSupplyOrdersTab() {
</div>
</td>
<td className="p-4">{getStatusBadge(order.status)}</td>
<td className="p-4">
<div className="flex items-center space-x-2">
{order.status === "PENDING" && (
<>
<Button
size="sm"
onClick={() =>
handleStatusUpdate(order.id, "CONFIRMED")
}
disabled={updating}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30"
>
<CheckCircle className="h-4 w-4 mr-1" />
Одобрить
</Button>
<Button
size="sm"
onClick={() =>
handleStatusUpdate(order.id, "CANCELLED")
}
disabled={updating}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
>
<XCircle className="h-4 w-4 mr-1" />
Отказать
</Button>
</>
)}
{order.status === "CONFIRMED" && (
<Button
size="sm"
onClick={() =>
handleStatusUpdate(order.id, "IN_TRANSIT")
}
disabled={updating}
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30"
>
<Truck className="h-4 w-4 mr-1" />
Отправить
</Button>
)}
{order.status === "CANCELLED" && (
<span className="text-red-400 text-sm">
Отклонена
</span>
)}
{order.status === "IN_TRANSIT" && (
<span className="text-yellow-400 text-sm">
В пути
</span>
)}
{order.status === "DELIVERED" && (
<span className="text-green-400 text-sm">
Доставлена
</span>
)}
</div>
</td>
</tr>
{/* Развернутая информация о заказе */}
{isOrderExpanded && (
<tr>
<td colSpan={7} className="p-0">
<td colSpan={8} className="p-0">
<div className="bg-white/5 border-t border-white/10">
<div className="p-6">
<h4 className="text-white font-semibold mb-4">
Состав заказа:
Состав заявки:
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{order.items.map((item) => (

View File

@ -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<Set<string>>(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 <Badge className={`${color} border text-xs`}>{label}</Badge>;
};
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 (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<span className="ml-3 text-white/60">Загрузка заказов...</span>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-red-400 font-medium">Ошибка загрузки заказов</p>
<p className="text-white/60 text-sm mt-2">{error.message}</p>
</div>
</div>
);
}
return (
<div className="space-y-4">
{/* Статистика заказов поставок селлера */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Card className="bg-white/10 backdrop-blur border-white/20 p-3">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-blue-500/20 rounded-lg">
<Package2 className="h-4 w-4 text-blue-400" />
</div>
<div>
<p className="text-white/60 text-xs">Всего заказов</p>
<p className="text-lg font-bold text-white">{totalOrders}</p>
</div>
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-3">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-green-500/20 rounded-lg">
<DollarSign className="h-4 w-4 text-green-400" />
</div>
<div>
<p className="text-white/60 text-xs">Общая сумма</p>
<p className="text-lg font-bold text-white">
{formatCurrency(totalAmount)}
</p>
</div>
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-3">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-yellow-500/20 rounded-lg">
<Clock className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-white/60 text-xs">Ожидают</p>
<p className="text-lg font-bold text-white">{pendingOrders}</p>
</div>
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-3">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-purple-500/20 rounded-lg">
<Truck className="h-4 w-4 text-purple-400" />
</div>
<div>
<p className="text-white/60 text-xs">Доставлено</p>
<p className="text-lg font-bold text-white">{deliveredOrders}</p>
</div>
</div>
</Card>
</div>
{/* Таблица заказов поставок */}
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-3 text-white font-semibold text-sm">
</th>
<th className="text-left p-3 text-white font-semibold text-sm">
Поставщик
</th>
<th className="text-left p-3 text-white font-semibold text-sm">
Дата доставки
</th>
<th className="text-left p-3 text-white font-semibold text-sm">
Позиций
</th>
<th className="text-left p-3 text-white font-semibold text-sm">
Сумма
</th>
<th className="text-left p-3 text-white font-semibold text-sm">
Фулфилмент
</th>
<th className="text-left p-3 text-white font-semibold text-sm">
Статус
</th>
</tr>
</thead>
<tbody>
{sellerOrders.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-8">
<div className="text-white/60">
<Box className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>Заказов поставок пока нет</p>
<p className="text-sm mt-1">
Создайте первый заказ поставки расходников
</p>
</div>
</td>
</tr>
) : (
sellerOrders.map((order, index) => {
const isOrderExpanded = expandedOrders.has(order.id);
const orderNumber = sellerOrders.length - index;
return (
<React.Fragment key={order.id}>
{/* Основная строка заказа поставки */}
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer"
onClick={() => toggleOrderExpansion(order.id)}
>
<td className="p-3">
<div className="flex items-center space-x-2">
{isOrderExpanded ? (
<ChevronDown className="h-4 w-4 text-white/60" />
) : (
<ChevronRight className="h-4 w-4 text-white/60" />
)}
<span className="text-white font-medium">
{orderNumber}
</span>
</div>
</td>
<td className="p-3">
<div className="flex items-center space-x-2">
<Building2 className="h-4 w-4 text-white/40" />
<div>
<p className="text-white text-sm font-medium">
{order.partner.name ||
order.partner.fullName ||
"Не указан"}
</p>
{order.partner.inn && (
<p className="text-white/60 text-xs">
ИНН: {order.partner.inn}
</p>
)}
</div>
</div>
</td>
<td className="p-3">
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-white/40" />
<span className="text-white text-sm">
{formatDate(order.deliveryDate)}
</span>
</div>
</td>
<td className="p-3">
<span className="text-white text-sm">
{order.totalItems}
</span>
</td>
<td className="p-3">
<span className="text-white text-sm font-medium">
{formatCurrency(order.totalAmount)}
</span>
</td>
<td className="p-3">
<div className="flex items-center space-x-2">
<Truck className="h-4 w-4 text-white/40" />
<span className="text-white/80 text-sm">
{order.fulfillmentCenter?.name ||
order.fulfillmentCenter?.fullName ||
"Не указан"}
</span>
</div>
</td>
<td className="p-3">{getStatusBadge(order.status)}</td>
</tr>
{/* Развернутая информация о заказе */}
{isOrderExpanded && (
<tr>
<td colSpan={7} className="p-0">
<div className="bg-white/5 border-t border-white/10">
<div className="p-4 space-y-3">
<h4 className="text-white font-medium text-sm mb-2">
Состав заказа:
</h4>
<div className="space-y-2">
{order.items.map((item) => (
<div
key={item.id}
className="flex items-center justify-between bg-white/5 rounded-lg p-3"
>
<div className="flex items-center space-x-3">
<Package2 className="h-4 w-4 text-white/40" />
<div>
<p className="text-white text-sm font-medium">
{item.product.name}
</p>
{item.product.category && (
<p className="text-white/60 text-xs">
{item.product.category.name}
</p>
)}
</div>
</div>
<div className="text-right">
<p className="text-white text-sm">
{item.quantity} шт ×{" "}
{formatCurrency(item.price)}
</p>
<p className="text-white/60 text-xs">
= {formatCurrency(item.totalPrice)}
</p>
</div>
</div>
))}
</div>
<div className="flex justify-between items-center pt-2 border-t border-white/10">
<span className="text-white/60 text-sm">
Создан: {formatDate(order.createdAt)}
</span>
<span className="text-white font-medium">
Итого: {formatCurrency(order.totalAmount)}
</span>
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})
)}
</tbody>
</table>
</div>
</Card>
</div>
);
}

View File

@ -1,87 +1,108 @@
"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]
}
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: 'Мало'
}
if (quantity === 0)
return {
color: 'text-green-400',
bgColor: 'bg-green-500/10',
label: 'В наличии'
}
}
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 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 (
<div className="group">
{/* Основная строка товара */}
<div
className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 transition-all duration-300"
>
<div className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 transition-all duration-300">
{/* Товар (3 колонки) */}
<div className="col-span-3 flex items-center gap-3">
<div className="w-12 h-12 rounded-lg overflow-hidden bg-white/10 flex-shrink-0">
@ -91,8 +112,16 @@ export function StockTableRow({ item }: StockTableRowProps) {
alt={item.title}
className="w-full h-full object-cover"
onError={(e) => {
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) {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded">
{item.brand || 'Без бренда'}
{item.brand || "Без бренда"}
</span>
<span className="text-white/40 text-xs">#{item.nmId}</span>
</div>
<h3 className="text-white text-sm font-medium line-clamp-1 mb-1">
{item.title}
</h3>
<div className="text-white/60 text-xs">
{item.vendorCode}
</div>
<div className="text-white/60 text-xs">{item.vendorCode}</div>
</div>
</div>
@ -123,7 +150,9 @@ export function StockTableRow({ item }: StockTableRowProps) {
<div className={`text-lg font-bold ${stockStatus.color}`}>
{item.totalQuantity.toLocaleString()}
</div>
<div className={`text-xs px-2 py-0.5 rounded ${stockStatus.bgColor} ${stockStatus.color}`}>
<div
className={`text-xs px-2 py-0.5 rounded ${stockStatus.bgColor} ${stockStatus.color}`}
>
{stockStatus.label}
</div>
</div>
@ -164,16 +193,22 @@ export function StockTableRow({ item }: StockTableRowProps) {
<div className="space-y-1 w-full">
{keyCharacteristics.map((char, index) => (
<div key={index} className="flex justify-between text-xs">
<span className="text-white/60 truncate w-1/2">{char.name}:</span>
<span className="text-white/60 truncate w-1/2">
{char.name}:
</span>
<span className="text-white truncate w-1/2 text-right">
{Array.isArray(char.value) ? char.value.join(', ') : String(char.value)}
{Array.isArray(char.value)
? char.value.join(", ")
: String(char.value)}
</span>
</div>
))}
{item.subjectName && (
<div className="flex justify-between text-xs">
<span className="text-white/60">Категория:</span>
<span className="text-blue-300 truncate text-right">{item.subjectName}</span>
<span className="text-blue-300 truncate text-right">
{item.subjectName}
</span>
</div>
)}
</div>
@ -196,7 +231,11 @@ export function StockTableRow({ item }: StockTableRowProps) {
{/* Цифры */}
<div className="flex items-center gap-3 text-xs">
<div className="text-center">
<div className={`font-bold ${stock.quantity > 0 ? 'text-green-400' : 'text-white/30'}`}>
<div
className={`font-bold ${
stock.quantity > 0 ? "text-green-400" : "text-white/30"
}`}
>
{stock.quantity}
</div>
<div className="text-white/50">остаток</div>
@ -208,14 +247,18 @@ export function StockTableRow({ item }: StockTableRowProps) {
{stock.inWayToClient > 0 && (
<div className="text-center">
<div className="font-bold text-orange-400">{stock.inWayToClient}</div>
<div className="font-bold text-orange-400">
{stock.inWayToClient}
</div>
<div className="text-white/50">к клиенту</div>
</div>
)}
{stock.inWayFromClient > 0 && (
<div className="text-center">
<div className="font-bold text-red-400">{stock.inWayFromClient}</div>
<div className="font-bold text-red-400">
{stock.inWayFromClient}
</div>
<div className="text-white/50">от клиента</div>
</div>
)}
@ -227,5 +270,5 @@ export function StockTableRow({ item }: StockTableRowProps) {
</div>
</div>
</div>
)
);
}

View File

@ -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 { user } = useAuth();
const { isCollapsed, getSidebarMargin } = useSidebar();
const [stocks, setStocks] = useState<WBStock[]>([])
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([])
const [loading, setLoading] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [stocks, setStocks] = useState<WBStock[]>([]);
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([]);
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)
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<any[]>([])
const [analyticsData, setAnalyticsData] = useState<unknown[]>([]);
const hasWBApiKey = user?.wildberriesApiKey
const hasWBApiKey = user?.wildberriesApiKey;
// Комбинирование карточек с индивидуальными данными аналитики
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
const stocksMap = new Map<number, WBStock>()
const combineCardsWithAnalytics = (
cards: unknown[],
analyticsResults: unknown[]
) => {
const stocksMap = new Map<number, WBStock>();
// Создаем карту аналитических данных для быстрого поиска
const analyticsMap = new Map() // Map nmId to its analytics data
analyticsResults.forEach(result => {
analyticsMap.set(result.nmId, result.data)
})
const analyticsMap = new Map(); // Map nmId to its analytics data
analyticsResults.forEach((result) => {
analyticsMap.set(result.nmId, result.data);
});
cards.forEach(card => {
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
})
inWayFromClient: office.metrics?.fromClientCount || 0,
});
stock.totalQuantity += office.metrics?.stockCount || 0
stock.totalReserved += office.metrics?.toClientCount || 0
})
stock.totalQuantity += office.metrics?.stockCount || 0;
stock.totalReserved += office.metrics?.toClientCount || 0;
});
}
})
});
}
stocksMap.set(card.nmID, stock)
})
stocksMap.set(card.nmID, stock);
});
return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
}
return Array.from(stocksMap.values()).sort(
(a, b) => b.totalQuantity - a.totalQuantity
);
};
// Извлечение информации о складах из данных
const extractWarehousesFromStocks = (stocksData: WBStock[]): WBWarehouse[] => {
const warehousesMap = new Map<number, WBWarehouse>()
const extractWarehousesFromStocks = (
stocksData: WBStock[]
): WBWarehouse[] => {
const warehousesMap = new Map<number, WBWarehouse>();
stocksData.forEach(stock => {
stock.stocks.forEach(stockInfo => {
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 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 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)
const extractedWarehouses = extractWarehousesFromStocks(combinedStocks);
setStocks(combinedStocks)
setWarehouses(extractedWarehouses)
updateStatistics(combinedStocks, extractedWarehouses)
setStocks(combinedStocks);
setWarehouses(extractedWarehouses);
updateStatistics(combinedStocks, extractedWarehouses);
toast.success(`Загружено товаров: ${combinedStocks.length}`)
} catch (error: any) {
console.error('WB Warehouse: Error loading data:', error)
toast.error('Ошибка загрузки данных: ' + (error.message || 'Неизвестная ошибка'))
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 (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<div className="h-full w-full flex flex-col">
{/* Результирующие вкладки */}
<StatsCards
totalProducts={totalProducts}
@ -275,16 +309,25 @@ export function WBWarehouseDashboard() {
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{analyticsData.map((warehouse) => (
<Card key={warehouse.warehouseId} className="bg-white/5 border-white/10 p-3">
<div className="text-sm font-medium text-white mb-2">{warehouse.warehouseName}</div>
<Card
key={warehouse.warehouseId}
className="bg-white/5 border-white/10 p-3"
>
<div className="text-sm font-medium text-white mb-2">
{warehouse.warehouseName}
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-white/60">К клиенту:</span>
<span className="text-green-400 font-medium">{warehouse.toClient}</span>
<span className="text-green-400 font-medium">
{warehouse.toClient}
</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-white/60">От клиента:</span>
<span className="text-orange-400 font-medium">{warehouse.fromClient}</span>
<span className="text-orange-400 font-medium">
{warehouse.fromClient}
</span>
</div>
</div>
</Card>
@ -294,10 +337,7 @@ export function WBWarehouseDashboard() {
)}
{/* Поиск */}
<SearchBar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
/>
<SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
{/* Список товаров */}
<div className="flex-1 overflow-hidden">
@ -309,10 +349,15 @@ export function WBWarehouseDashboard() {
) : !hasWBApiKey ? (
<Card className="glass-card border-white/10 p-8 text-center">
<Package className="h-12 w-12 text-blue-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">Настройте API Wildberries</h3>
<p className="text-white/60 mb-4">Для просмотра остатков добавьте API ключ Wildberries в настройках</p>
<h3 className="text-lg font-medium text-white mb-2">
Настройте API Wildberries
</h3>
<p className="text-white/60 mb-4">
Для просмотра остатков добавьте API ключ Wildberries в
настройках
</p>
<Button
onClick={() => window.location.href = '/settings'}
onClick={() => (window.location.href = "/settings")}
className="bg-blue-600 hover:bg-blue-700"
>
Перейти в настройки
@ -321,8 +366,12 @@ export function WBWarehouseDashboard() {
) : filteredStocks.length === 0 ? (
<Card className="glass-card border-white/10 p-8 text-center">
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">Товары не найдены</h3>
<p className="text-white/60">Попробуйте изменить параметры поиска</p>
<h3 className="text-lg font-medium text-white mb-2">
Товары не найдены
</h3>
<p className="text-white/60">
Попробуйте изменить параметры поиска
</p>
</Card>
) : (
<div className="overflow-y-auto pr-2 max-h-full">
@ -340,5 +389,5 @@ export function WBWarehouseDashboard() {
</div>
</main>
</div>
)
);
}

View File

@ -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`
}
}
}
`
`;
// Мутация для обновления статуса заказа поставки
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
}
}
}
}
}
}
`;

View File

@ -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,7 +3263,7 @@ export const resolvers = {
totalItems: totalItems,
organizationId: currentUser.organization.id,
fulfillmentCenterId: fulfillmentCenterId,
status: initialStatus as any,
status: initialStatus,
items: {
create: orderItems,
},
@ -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: "Ошибка при обновлении статуса заказа поставки",
};
}
},
},
// Резолверы типов

View File

@ -181,6 +181,7 @@ export const typeDefs = gql`
# Заказы поставок расходников
createSupplyOrder(input: SupplyOrderInput!): SupplyOrderResponse!
updateSupplyOrderStatus(id: ID!, status: SupplyOrderStatus!): SupplyOrderResponse!
# Работа с логистикой
createLogistics(input: LogisticsInput!): LogisticsResponse!