Добавлен новый компонент для отображения бизнес-процессов в интерфейсе управления. Обновлен компонент 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 { SuppliesDemo } from "./ui-kit/supplies-demo";
import { WBWarehouseDemo } from "./ui-kit/wb-warehouse-demo"; import { WBWarehouseDemo } from "./ui-kit/wb-warehouse-demo";
import { SuppliesNavigationDemo } from "./ui-kit/supplies-navigation-demo"; import { SuppliesNavigationDemo } from "./ui-kit/supplies-navigation-demo";
import { BusinessProcessesDemo } from "./ui-kit/business-processes-demo";
export function UIKitSection() { export function UIKitSection() {
return ( return (
@ -154,6 +155,12 @@ export function UIKitSection() {
> >
Навигация поставок Навигация поставок
</TabsTrigger> </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> </TabsList>
<TabsContent value="buttons" className="space-y-6"> <TabsContent value="buttons" className="space-y-6">
@ -235,6 +242,10 @@ export function UIKitSection() {
<TabsContent value="supplies-navigation" className="space-y-6"> <TabsContent value="supplies-navigation" className="space-y-6">
<SuppliesNavigationDemo /> <SuppliesNavigationDemo />
</TabsContent> </TabsContent>
<TabsContent value="business-processes" className="space-y-6">
<BusinessProcessesDemo />
</TabsContent>
</Tabs> </Tabs>
</div> </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> </h2>
<p className="text-white/70 mb-6"> <p className="text-white/70 mb-6">
Компоненты навигации, используемые в разделе "Мои поставки" Компоненты навигации, используемые в разделе &quot;Мои поставки&quot;
</p> </p>
</div> </div>

View File

@ -7,7 +7,11 @@ import { Card } from "@/components/ui/card";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import { useQuery } from "@apollo/client"; 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 { import {
Settings, Settings,
LogOut, LogOut,
@ -23,6 +27,36 @@ import {
BarChart3, BarChart3,
} from "lucide-react"; } 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() { export function Sidebar() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const router = useRouter(); const router = useRouter();
@ -162,9 +196,7 @@ export function Sidebar() {
const isFulfillmentStatisticsActive = pathname.startsWith( const isFulfillmentStatisticsActive = pathname.startsWith(
"/fulfillment-statistics" "/fulfillment-statistics"
); );
const isSellerStatisticsActive = pathname.startsWith( const isSellerStatisticsActive = pathname.startsWith("/seller-statistics");
"/seller-statistics"
);
const isEmployeesActive = pathname.startsWith("/employees"); const isEmployeesActive = pathname.startsWith("/employees");
const isSuppliesActive = const isSuppliesActive =
pathname.startsWith("/supplies") || pathname.startsWith("/supplies") ||
@ -483,45 +515,45 @@ export function Sidebar() {
</Button> </Button>
)} )}
{/* Склад - для фулфилмент */} {/* Склад - для фулфилмент */}
{user?.organization?.type === "FULFILLMENT" && ( {user?.organization?.type === "FULFILLMENT" && (
<Button <Button
variant={isFulfillmentWarehouseActive ? "secondary" : "ghost"} variant={isFulfillmentWarehouseActive ? "secondary" : "ghost"}
className={`w-full ${ className={`w-full ${
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10" isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
} text-left transition-all duration-200 text-xs ${ } text-left transition-all duration-200 text-xs ${
isFulfillmentWarehouseActive isFulfillmentWarehouseActive
? "bg-white/20 text-white hover:bg-white/30" ? "bg-white/20 text-white hover:bg-white/30"
: "text-white/80 hover:bg-white/10 hover:text-white" : "text-white/80 hover:bg-white/10 hover:text-white"
} cursor-pointer`} } cursor-pointer`}
onClick={handleFulfillmentWarehouseClick} onClick={handleFulfillmentWarehouseClick}
title={isCollapsed ? "Склад" : ""} title={isCollapsed ? "Склад" : ""}
> >
<Warehouse className="h-4 w-4 flex-shrink-0" /> <Warehouse className="h-4 w-4 flex-shrink-0" />
{!isCollapsed && <span className="ml-3">Склад</span>} {!isCollapsed && <span className="ml-3">Склад</span>}
</Button> </Button>
)} )}
{/* Статистика - для фулфилмент */} {/* Статистика - для фулфилмент */}
{user?.organization?.type === "FULFILLMENT" && ( {user?.organization?.type === "FULFILLMENT" && (
<Button <Button
variant={isFulfillmentStatisticsActive ? "secondary" : "ghost"} variant={isFulfillmentStatisticsActive ? "secondary" : "ghost"}
className={`w-full ${ className={`w-full ${
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10" isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
} text-left transition-all duration-200 text-xs ${ } text-left transition-all duration-200 text-xs ${
isFulfillmentStatisticsActive isFulfillmentStatisticsActive
? "bg-white/20 text-white hover:bg-white/30" ? "bg-white/20 text-white hover:bg-white/30"
: "text-white/80 hover:bg-white/10 hover:text-white" : "text-white/80 hover:bg-white/10 hover:text-white"
} cursor-pointer`} } cursor-pointer`}
onClick={handleFulfillmentStatisticsClick} onClick={handleFulfillmentStatisticsClick}
title={isCollapsed ? "Статистика" : ""} title={isCollapsed ? "Статистика" : ""}
> >
<BarChart3 className="h-4 w-4 flex-shrink-0" /> <BarChart3 className="h-4 w-4 flex-shrink-0" />
{!isCollapsed && <span className="ml-3">Статистика</span>} {!isCollapsed && <span className="ml-3">Статистика</span>}
</Button> </Button>
)} )}
{/* Отгрузки - для оптовиков */} {/* Заявки - для оптовиков */}
{user?.organization?.type === "WHOLESALE" && ( {user?.organization?.type === "WHOLESALE" && (
<Button <Button
variant={isSuppliesActive ? "secondary" : "ghost"} variant={isSuppliesActive ? "secondary" : "ghost"}
@ -531,12 +563,16 @@ export function Sidebar() {
isSuppliesActive isSuppliesActive
? "bg-white/20 text-white hover:bg-white/30" ? "bg-white/20 text-white hover:bg-white/30"
: "text-white/80 hover:bg-white/10 hover:text-white" : "text-white/80 hover:bg-white/10 hover:text-white"
} cursor-pointer`} } cursor-pointer relative`}
onClick={handleSuppliesClick} onClick={handleSuppliesClick}
title={isCollapsed ? "Отгрузки" : ""} title={isCollapsed ? "Заявки" : ""}
> >
<Truck className="h-4 w-4 flex-shrink-0" /> <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> </Button>
)} )}

View File

@ -1,95 +1,100 @@
"use client"; "use client";
import React, { useState } from "react"; import { useState } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { StatsCard } from "../../supplies/ui/stats-card"; import { Button } from "@/components/ui/button";
import { StatsGrid } from "../../supplies/ui/stats-grid"; import { Separator } from "@/components/ui/separator";
import { useQuery } from "@apollo/client"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; 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 { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { import {
Calendar, Calendar,
Building2, Package,
TrendingUp, Truck,
DollarSign,
Wrench,
Package2,
ChevronDown,
ChevronRight,
User, User,
CheckCircle,
Clock,
AlertCircle,
XCircle,
MapPin,
Phone,
Mail,
Layers,
Building,
Hash,
Store,
} from "lucide-react"; } 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 { interface SupplyOrder {
id: string; id: string;
organizationId: string; partnerId: string;
deliveryDate: string; deliveryDate: string;
status: string; status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
totalAmount: number; totalAmount: number;
totalItems: number; totalItems: number;
fulfillmentCenterId?: string;
createdAt: string; createdAt: string;
updatedAt: string;
partner: { partner: {
id: string; id: string;
name?: string;
fullName?: string;
inn: string; inn: string;
name: string;
fullName: string;
address?: string; address?: string;
phones?: string[]; phones?: string[];
emails?: string[]; emails?: string[];
}; };
organization: { items: Array<{
id: string; id: string;
name?: string; quantity: number;
fullName?: string; price: number;
type: string; totalPrice: number;
}; product: {
fulfillmentCenter?: { id: string;
id: string; name: string;
name?: string; article: string;
fullName?: string; description?: string;
type: string; price: number;
}; quantity: number;
items: SupplyOrderItem[]; images?: string[];
mainImage?: string;
category?: {
id: string;
name: string;
};
};
}>;
} }
export function FulfillmentConsumablesOrdersTab() { export function FulfillmentConsumablesOrdersTab() {
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set()); const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
const { user } = useAuth(); const { user } = useAuth();
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, { // Загружаем заказы поставок
fetchPolicy: 'cache-and-network', // Принудительно проверяем сервер const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS);
notifyOnNetworkStatusChange: true
});
// Получаем ID текущей организации (фулфилмент-центра) // Мутация для обновления статуса заказа
const currentOrganizationId = user?.organization?.id; const [updateSupplyOrderStatus, { loading: updating }] = useMutation(
UPDATE_SUPPLY_ORDER_STATUS,
// Фильтруем заказы где текущая организация является получателем (заказы ОТ селлеров) {
const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( onCompleted: (data) => {
(order: SupplyOrder) => { if (data.updateSupplyOrderStatus.success) {
// Показываем заказы где текущий фулфилмент-центр указан как получатель toast.success(data.updateSupplyOrderStatus.message);
// И заказчик НЕ является самим фулфилмент-центром (исключаем наши собственные заказы) refetch(); // Обновляем список заказов
return order.fulfillmentCenterId === currentOrganizationId && } else {
order.organizationId !== currentOrganizationId; 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); setExpandedOrders(newExpanded);
}; };
const getStatusBadge = (status: string) => { // Получаем данные заказов поставок
const statusMap: Record<string, { label: string; color: string }> = { const supplyOrders: SupplyOrder[] = data?.supplyOrders || [];
CREATED: {
label: "Новый заказ", // Фильтруем заказы для фулфилмента (где текущий фулфилмент является получателем)
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", color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
icon: Clock,
}, },
CONFIRMED: { CONFIRMED: {
label: "Подтвержден", label: "Подтверждена",
color: "bg-green-500/20 text-green-300 border-green-500/30", color: "bg-green-500/20 text-green-300 border-green-500/30",
icon: CheckCircle,
}, },
IN_PROGRESS: { IN_TRANSIT: {
label: "Обрабатывается", label: "В пути",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
icon: Truck,
}, },
DELIVERED: { DELIVERED: {
label: "Доставлен", label: "Доставлена",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30", color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
icon: Package,
}, },
CANCELLED: { CANCELLED: {
label: "Отменен", label: "Отменена",
color: "bg-red-500/20 text-red-300 border-red-500/30", color: "bg-red-500/20 text-red-300 border-red-500/30",
icon: XCircle,
}, },
}; };
const { label, color, icon: Icon } = statusMap[status];
const { label, color } = statusMap[status] || { return (
label: status, <Badge className={`${color} border flex items-center gap-1 text-xs`}>
color: "bg-gray-500/20 text-gray-300 border-gray-500/30", <Icon className="h-3 w-3" />
}; {label}
</Badge>
return <Badge className={`${color} border`}>{label}</Badge>; );
}; };
const formatCurrency = (amount: number) => { const handleStatusUpdate = async (
return new Intl.NumberFormat("ru-RU", { orderId: string,
style: "currency", newStatus: SupplyOrder["status"]
currency: "RUB", ) => {
minimumFractionDigits: 0, try {
}).format(amount); await updateSupplyOrderStatus({
variables: {
id: orderId,
status: newStatus,
},
});
} catch (error) {
console.error("Error updating status:", error);
}
};
const canMarkAsDelivered = (status: SupplyOrder["status"]) => {
return status === "IN_TRANSIT";
}; };
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
@ -151,266 +193,437 @@ export function FulfillmentConsumablesOrdersTab() {
}); });
}; };
const formatDateTime = (dateString: string) => { const formatCurrency = (amount: number) => {
return new Date(dateString).toLocaleString("ru-RU", { return new Intl.NumberFormat("ru-RU", {
day: "2-digit", style: "currency",
month: "2-digit", currency: "RUB",
year: "numeric", }).format(amount);
hour: "2-digit",
minute: "2-digit",
});
}; };
// Статистика для фулфилмент-центра const getInitials = (name: string): string => {
const totalOrders = incomingSupplyOrders.length; return name
const totalAmount = incomingSupplyOrders.reduce((sum, order) => sum + order.totalAmount, 0); .split(" ")
const totalItems = incomingSupplyOrders.reduce((sum, order) => sum + order.totalItems, 0); .map((word) => word.charAt(0))
const newOrders = incomingSupplyOrders.filter(order => order.status === "CREATED").length; .join("")
.toUpperCase()
.slice(0, 2);
};
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div> <div className="text-white/60">Загрузка заказов поставок...</div>
<span className="ml-3 text-white/60">Загрузка заказов расходников...</span>
</div> </div>
); );
} }
if (error) { if (error) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center p-8">
<div className="text-center"> <div className="text-red-400">Ошибка загрузки заказов поставок</div>
<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> </div>
); );
} }
return ( return (
<div className="space-y-6"> <div className="space-y-2">
{/* Статистика входящих заказов расходников */} {/* Компактная статистика */}
<StatsGrid> <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
<StatsCard <Card className="bg-white/10 backdrop-blur border-white/20 p-2">
title="Входящие заказы" <div className="flex items-center space-x-2">
value={totalOrders} <div className="p-1 bg-blue-500/20 rounded">
icon={Package2} <Clock className="h-3 w-3 text-blue-400" />
iconColor="text-orange-400" </div>
iconBg="bg-orange-500/20" <div>
subtitle="Заказы от селлеров" <p className="text-white/60 text-xs">Ожидание</p>
/> <p className="text-sm font-bold text-white">
{
<StatsCard fulfillmentOrders.filter(
title="Общая сумма" (order) => order.status === "PENDING"
value={formatCurrency(totalAmount)} ).length
icon={TrendingUp} }
iconColor="text-green-400" </p>
iconBg="bg-green-500/20" </div>
subtitle="Стоимость заказов"
/>
<StatsCard
title="Всего единиц"
value={totalItems}
icon={Wrench}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
subtitle="Количество расходников"
/>
<StatsCard
title="Новые заказы"
value={newOrders}
icon={Calendar}
iconColor="text-purple-400"
iconBg="bg-purple-500/20"
subtitle="Требуют обработки"
/>
</StatsGrid>
{/* Список входящих заказов расходников */}
{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">
Здесь будут отображаться заказы расходников от селлеров
</p>
</div> </div>
</Card> </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 ( <Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<React.Fragment key={order.id}> <div className="flex items-center space-x-2">
{/* Основная строка заказа */} <div className="p-1 bg-green-500/20 rounded">
<tr <CheckCircle className="h-3 w-3 text-green-400" />
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer" </div>
onClick={() => toggleOrderExpansion(order.id)} <div>
> <p className="text-white/60 text-xs">Подтверждено</p>
<td className="p-4"> <p className="text-sm font-bold text-white">
<div className="flex items-center space-x-2"> {
{isOrderExpanded ? ( fulfillmentOrders.filter(
<ChevronDown className="h-4 w-4 text-white/60" /> (order) => order.status === "CONFIRMED"
) : ( ).length
<ChevronRight className="h-4 w-4 text-white/60" /> }
</p>
</div>
</div>
</Card>
<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>
<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>
{/* Оптимизированный список заказов поставок */}
<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">
<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 text-xs">
Заказы поставок расходников будут отображаться здесь
</p>
</div>
</Card>
) : (
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)}
>
{/* Компактная основная информация */}
<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"> </AvatarFallback>
{order.id.slice(-8)} </Avatar>
</span> <div className="min-w-0 flex-1">
</div> <h3 className="text-white font-medium text-sm truncate max-w-[120px]">
</td> {order.partner.name || order.partner.fullName}
<td className="p-4"> </h3>
<div className="space-y-1"> <p className="text-white/60 text-xs">
<div className="flex items-center space-x-2"> {order.partner.inn}
<User className="h-4 w-4 text-white/40" /> </p>
<span className="text-white font-medium"> </div>
{order.organization.name || order.organization.fullName || "Селлер"} </div>
</span> </div>
</div>
<p className="text-white/60 text-sm">
Тип: {order.organization.type}
</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 || "Поставщик"}
</span>
</div>
<p className="text-white/60 text-sm">
ИНН: {order.partner.inn}
</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">
{formatDate(order.deliveryDate)}
</span>
</div>
</td>
<td className="p-4">
<span className="text-white/80">
{formatDateTime(order.createdAt)}
</span>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{order.totalItems} шт
</span>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
<DollarSign className="h-4 w-4 text-white/40" />
<span className="text-green-400 font-bold">
{formatCurrency(order.totalAmount)}
</span>
</div>
</td>
<td className="p-4">{getStatusBadge(order.status)}</td>
</tr>
{/* Развернутая информация о заказе */} {/* Поставщик (фулфилмент-центр) */}
{isOrderExpanded && ( <div className="hidden xl:flex items-center space-x-2 min-w-0">
<tr> <div className="flex flex-col items-center">
<td colSpan={8} className="p-0"> <Building className="h-3 w-3 text-green-400 mb-0.5" />
<div className="bg-white/5 border-t border-white/10"> <span className="text-green-400 text-xs font-medium leading-none">
<div className="p-6"> Поставщик
<h4 className="text-white font-semibold mb-4"> </span>
Состав заказа от селлера: </div>
</h4> <div className="flex items-center space-x-1.5">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <Avatar className="w-7 h-7 flex-shrink-0">
{order.items.map((item) => ( <AvatarFallback className="bg-green-500 text-white text-xs">
<Card {getInitials(user?.organization?.name || "ФФ")}
key={item.id} </AvatarFallback>
className="bg-white/10 backdrop-blur border-white/20 p-4" </Avatar>
> <div className="min-w-0">
<div className="space-y-3"> <h3 className="text-white font-medium text-sm truncate max-w-[100px]">
<div> {user?.organization?.name || "ФФ-центр"}
<h5 className="text-white font-medium mb-1"> </h3>
{item.product.name} <p className="text-white/60 text-xs">Наш ФФ</p>
</h5> </div>
<p className="text-white/60 text-sm"> </div>
Артикул: {item.product.article} </div>
</p>
{item.product.category && ( {/* Краткие данные */}
<Badge className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs mt-2"> <div className="hidden lg:flex items-center space-x-4">
{item.product.category.name} <div className="flex items-center space-x-1">
</Badge> <Calendar className="h-3 w-3 text-blue-400" />
)} <span className="text-white text-xs">
</div> {formatDate(order.deliveryDate)}
<div className="flex items-center justify-between"> </span>
<div className="text-sm"> </div>
<p className="text-white/60"> <div className="flex items-center space-x-1">
Количество: {item.quantity} шт <Package className="h-3 w-3 text-green-400" />
</p> <span className="text-white text-xs">
<p className="text-white/60"> {order.totalItems}
Цена: {formatCurrency(item.price)} </span>
</p> </div>
</div> <div className="flex items-center space-x-1">
<div className="text-right"> <Layers className="h-3 w-3 text-purple-400" />
<p className="text-green-400 font-semibold"> <span className="text-white text-xs">
{formatCurrency(item.totalPrice)} {order.items.length}
</p> </span>
</div> </div>
</div> </div>
</div> </div>
</Card>
))} {/* Правая часть - статус и действия */}
</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">
<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>
</div>
{/* Информация о поставщике */}
<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="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>
<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-xs">
{item.product.article}
</p>
{item.product.category && (
<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>
</div> </div>
</td> <div className="text-right flex-shrink-0">
</tr> <p className="text-white font-semibold text-sm">
)} {item.quantity} шт.
</React.Fragment> </p>
); <p className="text-white/60 text-xs">
})} {formatCurrency(item.price)}
</tbody> </p>
</table> <p className="text-green-400 font-semibold text-sm">
</div> {formatCurrency(item.totalPrice)}
</Card> </p>
)} </div>
</div>
))}
</div>
</div>
</>
)}
</div>
</Card>
))
)}
</div>
</div> </div>
); );
} }

View File

@ -5,8 +5,12 @@ import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Sidebar } from "@/components/dashboard/sidebar"; import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar"; 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 { import {
Package, Package,
TrendingUp, TrendingUp,
@ -22,13 +26,54 @@ import {
Package2, Package2,
Eye, Eye,
EyeOff, EyeOff,
ChevronRight,
ChevronDown,
Layers,
Truck,
Clock,
} from "lucide-react"; } 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 { interface StoreData {
id: string; id: string;
name: string; name: string;
logo?: string; logo?: string;
avatar?: string; // Аватар пользователя организации
products: number; products: number;
goods: number; goods: number;
defects: number; defects: number;
@ -40,6 +85,8 @@ interface StoreData {
defectsChange: number; defectsChange: number;
sellerSuppliesChange: number; sellerSuppliesChange: number;
pvzReturnsChange: number; pvzReturnsChange: number;
// Детализация по товарам
items: ProductItem[];
} }
interface WarehouseStats { interface WarehouseStats {
@ -51,6 +98,60 @@ interface WarehouseStats {
sellerSupplies: { current: number; change: number }; 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() { export function FulfillmentWarehouseDashboard() {
const { getSidebarMargin } = useSidebar(); const { getSidebarMargin } = useSidebar();
@ -59,101 +160,314 @@ export function FulfillmentWarehouseDashboard() {
const [sortField, setSortField] = useState<keyof StoreData>("name"); const [sortField, setSortField] = useState<keyof StoreData>("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set()); const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set());
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const [showAdditionalValues, setShowAdditionalValues] = useState(true); const [showAdditionalValues, setShowAdditionalValues] = useState(true);
// Мок данные для статистики // Загружаем данные из GraphQL
const warehouseStats: WarehouseStats = { const {
products: { current: 2856, change: 124 }, data: counterpartiesData,
goods: { current: 1391, change: 87 }, loading: counterpartiesLoading,
defects: { current: 43, change: -12 }, error: counterpartiesError,
pvzReturns: { current: 256, change: 34 }, refetch: refetchCounterparties,
fulfillmentSupplies: { current: 189, change: 23 }, } = useQuery(GET_MY_COUNTERPARTIES, {
sellerSupplies: { current: 534, change: 67 }, 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 getColorForStore = (storeId: string): string => {
const mockStoreData: StoreData[] = useMemo( const colors = [
() => [ "bg-blue-500",
{ "bg-green-500",
id: "1", "bg-purple-500",
name: "Электроника Плюс", "bg-orange-500",
products: 456, "bg-pink-500",
goods: 234, "bg-indigo-500",
defects: 12, "bg-teal-500",
sellerSupplies: 89, "bg-red-500",
pvzReturns: 45, "bg-yellow-500",
productsChange: 23, "bg-cyan-500",
goodsChange: 15, ];
defectsChange: -3, const hash = storeId
sellerSuppliesChange: 12, .split("")
pvzReturnsChange: 8, .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",
}, },
{ "2": {
id: "2", // Второй поставщик - Розовый
name: "Мода и Стиль", bg: "bg-pink-500/5",
products: 678, border: "border-pink-500/30",
goods: 345, borderLeft: "border-l-pink-400",
defects: 8, text: "text-pink-100",
sellerSupplies: 123, indicator: "bg-pink-400 border-pink-300",
pvzReturns: 67, hover: "hover:bg-pink-500/10",
productsChange: 34, header: "bg-pink-500/20 border-pink-500/40",
goodsChange: 22,
defectsChange: -2,
sellerSuppliesChange: 18,
pvzReturnsChange: 12,
}, },
{ "3": {
id: "3", // Третий поставщик - Зеленый
name: "Дом и Сад", bg: "bg-emerald-500/5",
products: 289, border: "border-emerald-500/30",
goods: 156, borderLeft: "border-l-emerald-400",
defects: 5, text: "text-emerald-100",
sellerSupplies: 67, indicator: "bg-emerald-400 border-emerald-300",
pvzReturns: 23, hover: "hover:bg-emerald-500/10",
productsChange: 12, header: "bg-emerald-500/20 border-emerald-500/40",
goodsChange: 8,
defectsChange: -1,
sellerSuppliesChange: 9,
pvzReturnsChange: 4,
}, },
{ "4": {
id: "4", // Четвертый поставщик - Фиолетовый
name: "Спорт и Отдых", bg: "bg-purple-500/5",
products: 567, border: "border-purple-500/30",
goods: 289, borderLeft: "border-l-purple-400",
defects: 15, text: "text-purple-100",
sellerSupplies: 134, indicator: "bg-purple-400 border-purple-300",
pvzReturns: 78, hover: "hover:bg-purple-500/10",
productsChange: 28, header: "bg-purple-500/20 border-purple-500/40",
goodsChange: 19,
defectsChange: -4,
sellerSuppliesChange: 21,
pvzReturnsChange: 15,
}, },
{ "5": {
id: "5", // Пятый поставщик - Оранжевый
name: "Красота и Здоровье", bg: "bg-orange-500/5",
products: 234, border: "border-orange-500/30",
goods: 123, borderLeft: "border-l-orange-400",
defects: 3, text: "text-orange-100",
sellerSupplies: 45, indicator: "bg-orange-400 border-orange-300",
pvzReturns: 19, hover: "hover:bg-orange-500/10",
productsChange: 8, header: "bg-orange-500/20 border-orange-500/40",
goodsChange: 5,
defectsChange: 0,
sellerSuppliesChange: 6,
pvzReturnsChange: 3,
}, },
], "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 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()) store.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
console.log("📋 Отфильтрованные поставщики:", {
filteredLength: filtered.length,
storeNames: filtered.map((s) => s.name),
});
filtered.sort((a, b) => { filtered.sort((a, b) => {
const aValue = a[sortField]; const aValue = a[sortField];
const bValue = b[sortField]; const bValue = b[sortField];
@ -172,7 +486,7 @@ export function FulfillmentWarehouseDashboard() {
}); });
return filtered; return filtered;
}, [searchTerm, sortField, sortOrder, mockStoreData]); }, [searchTerm, sortField, sortOrder, storeData]);
// Подсчет общих сумм // Подсчет общих сумм
const totals = useMemo(() => { const totals = useMemo(() => {
@ -224,6 +538,16 @@ export function FulfillmentWarehouseDashboard() {
setExpandedStores(newExpanded); 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) => { const handleSort = (field: keyof StoreData) => {
if (sortField === field) { if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc"); setSortOrder(sortOrder === "asc" ? "desc" : "asc");
@ -250,7 +574,7 @@ export function FulfillmentWarehouseDashboard() {
// Генерируем случайные значения для положительных и отрицательных изменений // Генерируем случайные значения для положительных и отрицательных изменений
const positiveChange = Math.floor(Math.random() * 50) + 10; // от 10 до 59 const positiveChange = Math.floor(Math.random() * 50) + 10; // от 10 до 59
const negativeChange = Math.floor(Math.random() * 30) + 5; // от 5 до 34 const negativeChange = Math.floor(Math.random() * 30) + 5; // от 5 до 34
const percentChange = (change / current) * 100; const percentChange = current > 0 ? (change / current) * 100 : 0;
return ( return (
<div <div
@ -314,8 +638,8 @@ export function FulfillmentWarehouseDashboard() {
sortable?: boolean; sortable?: boolean;
}) => ( }) => (
<div <div
className={`px-3 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider ${ 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-white/5" : "" sortable ? "cursor-pointer hover:text-white hover:bg-blue-500/10" : ""
} flex items-center space-x-1`} } flex items-center space-x-1`}
onClick={sortable && field ? () => handleSort(field) : undefined} onClick={sortable && field ? () => handleSort(field) : undefined}
> >
@ -350,6 +674,45 @@ export function FulfillmentWarehouseDashboard() {
</div> </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 ( return (
<div className="h-screen flex overflow-hidden"> <div className="h-screen flex overflow-hidden">
<Sidebar /> <Sidebar />
@ -359,9 +722,42 @@ export function FulfillmentWarehouseDashboard() {
{/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */} {/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}> <div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
<div className="glass-card p-4"> <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> Статистика склада
</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"> <div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
<StatCard <StatCard
title="Продукты" title="Продукты"
@ -421,8 +817,24 @@ export function FulfillmentWarehouseDashboard() {
style={{ maxHeight: "10vh" }} style={{ maxHeight: "10vh" }}
> >
<div className="flex items-center justify-between"> <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> </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" /> <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"> <div className="flex space-x-2">
<Input <Input
placeholder="Поиск по магазинам..." placeholder="Поиск по селлерам..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40 flex-1" 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" variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs" className="bg-blue-500/20 text-blue-300 text-xs"
> >
{filteredAndSortedStores.length} магазинов {filteredAndSortedStores.length} селлеров
</Badge> </Badge>
</div> </div>
</div> </div>
{/* Фиксированные заголовки таблицы */} {/* Фиксированные заголовки таблицы - Уровень 1 (Поставщики) */}
<div className="flex-shrink-0 bg-white/5 border-b border-white/10"> <div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
<div className="grid grid-cols-6 gap-0"> <div className="grid grid-cols-6 gap-0">
<TableHeader field="name" sortable> <TableHeader field="name" sortable>
/ Магазин / Селлер
</TableHeader> </TableHeader>
<TableHeader field="products" sortable> <TableHeader field="products" sortable>
Продукты Продукты
@ -477,8 +889,8 @@ export function FulfillmentWarehouseDashboard() {
</div> </div>
</div> </div>
{/* Строка с суммами */} {/* Строка с суммами - Уровень 1 (Поставщики) */}
<div className="flex-shrink-0 bg-blue-500/10 border-b border-blue-500/20"> <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="grid grid-cols-6 gap-0">
<div className="px-3 py-2 text-xs font-bold text-blue-300"> <div className="px-3 py-2 text-xs font-bold text-blue-300">
ИТОГО ({filteredAndSortedStores.length}) ИТОГО ({filteredAndSortedStores.length})
@ -499,29 +911,28 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400" : "text-red-400"
}`} }`}
> >
{( {totals.products > 0
(totals.productsChange / totals.products) * ? (
100 (totals.productsChange / totals.products) *
).toFixed(1)} 100
).toFixed(1)
: "0.0"}
% %
</span> </span>
</div> </div>
</div> </div>
{showAdditionalValues && ( {showAdditionalValues && (
<div className="flex items-center justify-end space-x-1"> <div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400"> <span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.productsChange * 0.6))} +{Math.abs(Math.floor(totals.productsChange * 0.6))}
</span> </span>
</div> </div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400"> <span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.productsChange * 0.4))} -{Math.abs(Math.floor(totals.productsChange * 0.4))}
</span> </span>
</div> </div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white"> <span className="text-[9px] font-bold text-white">
{Math.abs(totals.productsChange)} {Math.abs(totals.productsChange)}
@ -546,26 +957,27 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400" : "text-red-400"
}`} }`}
> >
{((totals.goodsChange / totals.goods) * 100).toFixed(1)} {totals.goods > 0
? ((totals.goodsChange / totals.goods) * 100).toFixed(
1
)
: "0.0"}
% %
</span> </span>
</div> </div>
</div> </div>
{showAdditionalValues && ( {showAdditionalValues && (
<div className="flex items-center justify-end space-x-1"> <div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400"> <span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.goodsChange * 0.6))} +{Math.abs(Math.floor(totals.goodsChange * 0.6))}
</span> </span>
</div> </div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400"> <span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.goodsChange * 0.4))} -{Math.abs(Math.floor(totals.goodsChange * 0.4))}
</span> </span>
</div> </div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white"> <span className="text-[9px] font-bold text-white">
{Math.abs(totals.goodsChange)} {Math.abs(totals.goodsChange)}
@ -590,29 +1002,28 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400" : "text-red-400"
}`} }`}
> >
{( {totals.defects > 0
(totals.defectsChange / totals.defects) * ? (
100 (totals.defectsChange / totals.defects) *
).toFixed(1)} 100
).toFixed(1)
: "0.0"}
% %
</span> </span>
</div> </div>
</div> </div>
{showAdditionalValues && ( {showAdditionalValues && (
<div className="flex items-center justify-end space-x-1"> <div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400"> <span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.defectsChange * 0.6))} +{Math.abs(Math.floor(totals.defectsChange * 0.6))}
</span> </span>
</div> </div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400"> <span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.defectsChange * 0.4))} -{Math.abs(Math.floor(totals.defectsChange * 0.4))}
</span> </span>
</div> </div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white"> <span className="text-[9px] font-bold text-white">
{Math.abs(totals.defectsChange)} {Math.abs(totals.defectsChange)}
@ -637,18 +1048,19 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400" : "text-red-400"
}`} }`}
> >
{( {totals.sellerSupplies > 0
(totals.sellerSuppliesChange / ? (
totals.sellerSupplies) * (totals.sellerSuppliesChange /
100 totals.sellerSupplies) *
).toFixed(1)} 100
).toFixed(1)
: "0.0"}
% %
</span> </span>
</div> </div>
</div> </div>
{showAdditionalValues && ( {showAdditionalValues && (
<div className="flex items-center justify-end space-x-1"> <div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400"> <span className="text-[9px] font-bold text-green-400">
+ +
@ -657,7 +1069,6 @@ export function FulfillmentWarehouseDashboard() {
)} )}
</span> </span>
</div> </div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400"> <span className="text-[9px] font-bold text-red-400">
- -
@ -666,7 +1077,6 @@ export function FulfillmentWarehouseDashboard() {
)} )}
</span> </span>
</div> </div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white"> <span className="text-[9px] font-bold text-white">
{Math.abs(totals.sellerSuppliesChange)} {Math.abs(totals.sellerSuppliesChange)}
@ -691,29 +1101,28 @@ export function FulfillmentWarehouseDashboard() {
: "text-red-400" : "text-red-400"
}`} }`}
> >
{( {totals.pvzReturns > 0
(totals.pvzReturnsChange / totals.pvzReturns) * ? (
100 (totals.pvzReturnsChange / totals.pvzReturns) *
).toFixed(1)} 100
).toFixed(1)
: "0.0"}
% %
</span> </span>
</div> </div>
</div> </div>
{showAdditionalValues && ( {showAdditionalValues && (
<div className="flex items-center justify-end space-x-1"> <div className="flex items-center justify-end space-x-1">
{/* Положительное изменение - всегда зеленое */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400"> <span className="text-[9px] font-bold text-green-400">
+{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))} +{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))}
</span> </span>
</div> </div>
{/* Отрицательное изменение - всегда красное */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400"> <span className="text-[9px] font-bold text-red-400">
-{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))} -{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))}
</span> </span>
</div> </div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5"> <div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white"> <span className="text-[9px] font-bold text-white">
{Math.abs(totals.pvzReturnsChange)} {Math.abs(totals.pvzReturnsChange)}
@ -727,275 +1136,534 @@ export function FulfillmentWarehouseDashboard() {
{/* Скроллируемый контент таблицы - оставшееся пространство */} {/* Скроллируемый контент таблицы - оставшееся пространство */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent"> <div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
{filteredAndSortedStores.map((store, index) => ( {filteredAndSortedStores.length === 0 ? (
<div <div className="flex items-center justify-center h-full">
key={store.id} <div className="text-center">
className="border-b border-white/10 hover:bg-white/5 transition-colors" <Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
> <p className="text-white/60 font-medium">
{/* Основная строка магазина */} {sellerPartners.length === 0
<div ? "Нет партнеров-селлеров"
className="grid grid-cols-6 gap-0 cursor-pointer" : "Партнеры не найдены"}
onClick={() => toggleStoreExpansion(store.id)} </p>
> <p className="text-white/40 text-sm mt-2">
<div className="px-3 py-2.5 flex items-center space-x-2"> {sellerPartners.length === 0
<span className="text-white/60 text-xs"> ? "Добавьте партнеров-селлеров для отображения данных склада"
{filteredAndSortedStores.length - index} : searchTerm
</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"> </p>
<Store className="h-3 w-3 text-white" />
</div>
<div>
<div className="text-white font-medium text-xs">
{store.name}
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold 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">
+
{Math.abs(
Math.floor(store.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(store.productsChange * 0.4)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.productsChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold 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))}
</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))}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.goodsChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold 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">
+
{Math.abs(
Math.floor(store.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(store.defectsChange * 0.4)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.defectsChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold 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)
)}
</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)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.sellerSuppliesChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div className="text-white font-semibold 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">
+
{Math.abs(
Math.floor(store.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(store.pvzReturnsChange * 0.4)
)}
</span>
</div>
{/* Результирующее изменение */}
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.pvzReturnsChange)}
</span>
</div>
</div>
)}
</div>
</div>
</div> </div>
</div>
{/* Расширенная информация */} ) : (
{expandedStores.has(store.id) && ( filteredAndSortedStores.map((store, index) => {
<div className="bg-white/5 px-3 py-3 border-t border-white/10"> const colorScheme = getColorScheme(store.id);
<div className="grid grid-cols-2 md:grid-cols-4 gap-3"> return (
<div className="glass-secondary rounded-lg p-2"> <div
<div className="flex items-center space-x-1.5 mb-1"> key={store.id}
<Package2 className="h-3 w-3 text-blue-400" /> className={`border-b ${colorScheme.border} ${colorScheme.hover} transition-colors border-l-8 ${colorScheme.borderLeft} ${colorScheme.bg} shadow-sm hover:shadow-md`}
<span className="text-blue-300 text-xs font-medium"> >
Продукты {/* Основная строка поставщика */}
</span> <div
</div> className="grid grid-cols-6 gap-0 cursor-pointer"
<div className="text-white text-sm font-bold"> onClick={() => toggleStoreExpansion(store.id)}
{formatNumber(store.products)} >
</div> <div className="px-3 py-2.5 flex items-center space-x-2">
<div className="text-blue-200/60 text-[10px]"> <span className="text-white/60 text-xs">
Готовые к отправке {filteredAndSortedStores.length - index}
</span>
<div className="flex items-center space-x-2">
<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 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> </div>
</div> </div>
<div className="glass-secondary rounded-lg p-2"> <div className="px-3 py-2.5">
<div className="flex items-center space-x-1.5 mb-1"> <div className="flex items-center justify-between">
<Package className="h-3 w-3 text-cyan-400" /> <div
<span className="text-cyan-300 text-xs font-medium"> className={`${colorScheme.text} font-bold text-sm`}
Товары >
</span> {formatNumber(store.products)}
</div> </div>
<div className="text-white text-sm font-bold"> {showAdditionalValues && (
{formatNumber(store.goods)} <div className="flex items-center space-x-1">
</div> <div className="flex items-center space-x-0.5">
<div className="text-cyan-200/60 text-[10px]"> <span className="text-[9px] font-bold text-green-400">
В обработке +
{Math.abs(
Math.floor(store.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(store.productsChange * 0.4)
)}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.productsChange)}
</span>
</div>
</div>
)}
</div> </div>
</div> </div>
<div className="glass-secondary rounded-lg p-2"> <div className="px-3 py-2.5">
<div className="flex items-center space-x-1.5 mb-1"> <div className="flex items-center justify-between">
<AlertTriangle className="h-3 w-3 text-red-400" /> <div
<span className="text-red-300 text-xs font-medium"> className={`${colorScheme.text} font-bold text-sm`}
Брак >
</span> {formatNumber(store.goods)}
</div> </div>
<div className="text-white text-sm font-bold"> {showAdditionalValues && (
{formatNumber(store.defects)} <div className="flex items-center space-x-1">
</div> <div className="flex items-center space-x-0.5">
<div className="text-red-200/60 text-[10px]"> <span className="text-[9px] font-bold text-green-400">
К утилизации +
{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)
)}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.goodsChange)}
</span>
</div>
</div>
)}
</div> </div>
</div> </div>
<div className="glass-secondary rounded-lg p-2"> <div className="px-3 py-2.5">
<div className="flex items-center space-x-1.5 mb-1"> <div className="flex items-center justify-between">
<RotateCcw className="h-3 w-3 text-yellow-400" /> <div
<span className="text-yellow-300 text-xs font-medium"> className={`${colorScheme.text} font-bold text-sm`}
Возвраты >
</span> {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">
+
{Math.abs(
Math.floor(store.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(store.defectsChange * 0.4)
)}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.defectsChange)}
</span>
</div>
</div>
)}
</div> </div>
<div className="text-white text-sm font-bold"> </div>
{formatNumber(store.pvzReturns)}
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<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
)
)}
</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
)
)}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.sellerSuppliesChange)}
</span>
</div>
</div>
)}
</div> </div>
<div className="text-yellow-200/60 text-[10px]"> </div>
С ПВЗ
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<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">
+
{Math.abs(
Math.floor(store.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(store.pvzReturnsChange * 0.4)
)}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.pvzReturnsChange)}
</span>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
{/* Второй уровень - детализация по товарам */}
{expandedStores.has(store.id) && (
<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>
</div>
</div>
{/* Динамическая часть - данные по товарам (скроллируемая) */}
<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/60 text-[10px]">
{item.article}
</div>
</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.productQuantity)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.productPlace || "-"}
</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.goodsQuantity)}
</div>
<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>
)}
</div>
))}
</div>
</div>
)}
</div> </div>
)} );
</div> })
))} )}
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,6 +4,7 @@ import React, { useState } from "react";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { useAuth } from "@/hooks/useAuth";
import { import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
@ -61,9 +62,12 @@ interface SupplyOrder {
export function SuppliesConsumablesTab() { export function SuppliesConsumablesTab() {
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set()); 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 toggleOrderExpansion = (orderId: string) => {
const newExpanded = new Set(expandedOrders); const newExpanded = new Set(expandedOrders);
@ -75,8 +79,11 @@ export function SuppliesConsumablesTab() {
setExpandedOrders(newExpanded); 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) => ({ const ordersWithNumbers = supplyOrders.map((order, index) => ({

View File

@ -18,13 +18,11 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { apolloClient } from "@/lib/apollo-client"; import { apolloClient } from "@/lib/apollo-client";
import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_LOGISTICS } from "@/graphql/queries";
import { import {
ArrowLeft, GET_MY_COUNTERPARTIES,
Package, GET_ORGANIZATION_LOGISTICS,
CalendarIcon, } from "@/graphql/queries";
Building, import { ArrowLeft, Package, CalendarIcon, Building } from "lucide-react";
} from "lucide-react";
// Компонент создания поставки товаров с новым интерфейсом // Компонент создания поставки товаров с новым интерфейсом
@ -47,16 +45,18 @@ export function CreateSupplyPage() {
const [goodsVolume, setGoodsVolume] = useState<number>(0); const [goodsVolume, setGoodsVolume] = useState<number>(0);
const [cargoPlaces, setCargoPlaces] = useState<number>(0); const [cargoPlaces, setCargoPlaces] = useState<number>(0);
const [goodsPrice, setGoodsPrice] = 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 [logisticsPrice, setLogisticsPrice] = useState<number>(0);
const [selectedServicesCost, setSelectedServicesCost] = 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); const [hasItemsInSupply, setHasItemsInSupply] = useState<boolean>(false);
// Загружаем контрагентов-фулфилментов // Загружаем контрагентов-фулфилментов
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES); const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
// Фильтруем только фулфилмент организации // Фильтруем только фулфилмент организации
const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter( const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter(
(org: Organization) => org.type === "FULFILLMENT" (org: Organization) => org.type === "FULFILLMENT"
); );
@ -87,19 +87,28 @@ export function CreateSupplyPage() {
}; };
// Функция для обновления информации о поставщиках (для расчета логистики) // Функция для обновления информации о поставщиках (для расчета логистики)
const handleSuppliersUpdate = (suppliersData: any[]) => { const handleSuppliersUpdate = (suppliersData: unknown[]) => {
// Находим рынок из выбранного поставщика // Находим рынок из выбранного поставщика
const selectedSupplier = suppliersData.find(supplier => supplier.selected); const selectedSupplier = suppliersData.find(
const supplierMarket = selectedSupplier?.market; (supplier: unknown) => (supplier as { selected?: boolean }).selected
);
console.log("Обновление поставщиков:", { selectedSupplier, supplierMarket, volume: goodsVolume }); const supplierMarket = (selectedSupplier as { market?: string })?.market;
console.log("Обновление поставщиков:", {
selectedSupplier,
supplierMarket,
volume: goodsVolume,
});
// Пересчитываем логистику с учетом рынка поставщика // Пересчитываем логистику с учетом рынка поставщика
calculateLogisticsPrice(goodsVolume, supplierMarket); calculateLogisticsPrice(goodsVolume, supplierMarket);
}; };
// Функция для расчета логистики по рынку поставщика и объему // Функция для расчета логистики по рынку поставщика и объему
const calculateLogisticsPrice = async (volume: number, supplierMarket?: string) => { const calculateLogisticsPrice = async (
volume: number,
supplierMarket?: string
) => {
// Логистика рассчитывается ТОЛЬКО если есть: // Логистика рассчитывается ТОЛЬКО если есть:
// 1. Выбранный фулфилмент // 1. Выбранный фулфилмент
// 2. Объем товаров > 0 // 2. Объем товаров > 0
@ -110,22 +119,35 @@ export function CreateSupplyPage() {
} }
try { try {
console.log(`Расчет логистики: ${supplierMarket}${selectedFulfillment}, объем: ${volume.toFixed(4)} м³`); console.log(
`Расчет логистики: ${supplierMarket}${selectedFulfillment}, объем: ${volume.toFixed(
4
)} м³`
);
// Получаем логистику выбранного фулфилмента из БД // Получаем логистику выбранного фулфилмента из БД
const { data: logisticsData } = await apolloClient.query({ const { data: logisticsData } = await apolloClient.query({
query: GET_ORGANIZATION_LOGISTICS, query: GET_ORGANIZATION_LOGISTICS,
variables: { organizationId: selectedFulfillment }, variables: { organizationId: selectedFulfillment },
fetchPolicy: 'network-only' fetchPolicy: "network-only",
}); });
const logistics = logisticsData?.organizationLogistics || []; const logistics = logisticsData?.organizationLogistics || [];
console.log(`Логистика фулфилмента ${selectedFulfillment}:`, logistics); console.log(`Логистика фулфилмента ${selectedFulfillment}:`, logistics);
// Ищем логистику для данного рынка // Ищем логистику для данного рынка
const logisticsRoute = logistics.find((route: any) => const logisticsRoute = logistics.find(
route.fromLocation.toLowerCase().includes(supplierMarket.toLowerCase()) || (route: {
supplierMarket.toLowerCase().includes(route.fromLocation.toLowerCase()) fromLocation: string;
toLocation: string;
pricePerCubicMeter: number;
}) =>
route.fromLocation
.toLowerCase()
.includes(supplierMarket.toLowerCase()) ||
supplierMarket
.toLowerCase()
.includes(route.fromLocation.toLowerCase())
); );
if (!logisticsRoute) { if (!logisticsRoute) {
@ -135,12 +157,21 @@ export function CreateSupplyPage() {
} }
// Выбираем цену в зависимости от объема // Выбираем цену в зависимости от объема
const pricePerM3 = volume <= 1 ? logisticsRoute.priceUnder1m3 : logisticsRoute.priceOver1m3; const pricePerM3 =
volume <= 1
? logisticsRoute.priceUnder1m3
: logisticsRoute.priceOver1m3;
const calculatedPrice = volume * pricePerM3; const calculatedPrice = volume * pricePerM3;
console.log(`Найдена логистика: ${logisticsRoute.fromLocation}${logisticsRoute.toLocation}`); console.log(
console.log(`Цена: ${pricePerM3}₽/м³ (${volume <= 1 ? 'до 1м³' : 'больше 1м³'}) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}`); `Найдена логистика: ${logisticsRoute.fromLocation} ${logisticsRoute.toLocation}`
);
console.log(
`Цена: ${pricePerM3}₽/м³ (${
volume <= 1 ? "до 1м³" : "больше 1м³"
}) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}`
);
setLogisticsPrice(calculatedPrice); setLogisticsPrice(calculatedPrice);
} catch (error) { } catch (error) {
console.error("Error calculating logistics price:", error); console.error("Error calculating logistics price:", error);
@ -149,7 +180,12 @@ export function CreateSupplyPage() {
}; };
const getTotalSum = () => { const getTotalSum = () => {
return goodsPrice + selectedServicesCost + selectedConsumablesCost + logisticsPrice; return (
goodsPrice +
selectedServicesCost +
selectedConsumablesCost +
logisticsPrice
);
}; };
const handleSupplyComplete = () => { const handleSupplyComplete = () => {
@ -258,7 +294,7 @@ export function CreateSupplyPage() {
<Select <Select
value={selectedFulfillment} value={selectedFulfillment}
onValueChange={(value) => { onValueChange={(value) => {
console.log('Выбран фулфилмент:', value); console.log("Выбран фулфилмент:", value);
setSelectedFulfillment(value); setSelectedFulfillment(value);
}} }}
> >
@ -282,7 +318,9 @@ export function CreateSupplyPage() {
</Label> </Label>
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3"> <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"> <span className="text-white/80 text-xs">
{goodsVolume > 0 ? `${goodsVolume.toFixed(2)} м³` : 'Рассчитывается автоматически'} {goodsVolume > 0
? `${goodsVolume.toFixed(2)} м³`
: "Рассчитывается автоматически"}
</span> </span>
</div> </div>
</div> </div>
@ -295,7 +333,9 @@ export function CreateSupplyPage() {
<Input <Input
type="number" type="number"
value={cargoPlaces || ""} value={cargoPlaces || ""}
onChange={(e) => setCargoPlaces(parseInt(e.target.value) || 0)} onChange={(e) =>
setCargoPlaces(parseInt(e.target.value) || 0)
}
placeholder="шт" 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" 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> </Label>
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3"> <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"> <span className="text-white/80 text-xs font-medium">
{goodsPrice > 0 ? formatCurrency(goodsPrice) : 'Рассчитывается автоматически'} {goodsPrice > 0
? formatCurrency(goodsPrice)
: "Рассчитывается автоматически"}
</span> </span>
</div> </div>
</div> </div>
@ -323,7 +365,9 @@ export function CreateSupplyPage() {
</Label> </Label>
<div className="h-8 bg-green-500/20 border border-green-400/30 rounded-lg flex items-center px-3"> <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"> <span className="text-green-400 text-xs font-medium">
{selectedServicesCost > 0 ? formatCurrency(selectedServicesCost) : 'Выберите услуги'} {selectedServicesCost > 0
? formatCurrency(selectedServicesCost)
: "Выберите услуги"}
</span> </span>
</div> </div>
</div> </div>
@ -335,7 +379,9 @@ export function CreateSupplyPage() {
</Label> </Label>
<div className="h-8 bg-orange-500/20 border border-orange-400/30 rounded-lg flex items-center px-3"> <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"> <span className="text-orange-400 text-xs font-medium">
{selectedConsumablesCost > 0 ? formatCurrency(selectedConsumablesCost) : 'Выберите расходники'} {selectedConsumablesCost > 0
? formatCurrency(selectedConsumablesCost)
: "Выберите расходники"}
</span> </span>
</div> </div>
</div> </div>
@ -347,12 +393,12 @@ export function CreateSupplyPage() {
</Label> </Label>
<div className="h-8 bg-blue-500/20 border border-blue-400/30 rounded-lg flex items-center px-3"> <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"> <span className="text-blue-400 text-xs font-medium">
{logisticsPrice > 0 ? formatCurrency(logisticsPrice) : 'Выберите поставщика'} {logisticsPrice > 0
? formatCurrency(logisticsPrice)
: "Выберите поставщика"}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
{/* 9. Итоговая сумма */} {/* 9. Итоговая сумма */}
@ -370,11 +416,21 @@ export function CreateSupplyPage() {
{/* 10. Кнопка создания поставки */} {/* 10. Кнопка создания поставки */}
<Button <Button
onClick={handleCreateSupplyClick} 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 ${ className={`w-full h-12 text-sm font-medium transition-all duration-200 ${
canCreateSupply && deliveryDate && selectedFulfillment && hasItemsInSupply && !isCreatingSupply canCreateSupply &&
? 'bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white' deliveryDate &&
: 'bg-gray-500/20 text-gray-400 cursor-not-allowed' 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 ? ( {isCreatingSupply ? (
@ -383,7 +439,7 @@ export function CreateSupplyPage() {
<span>Создание...</span> <span>Создание...</span>
</div> </div>
) : ( ) : (
'Создать поставку' "Создать поставку"
)} )}
</Button> </Button>
</Card> </Card>

View File

@ -113,7 +113,7 @@ interface DirectSupplyCreationProps {
onItemsCountChange?: (hasItems: boolean) => void; onItemsCountChange?: (hasItems: boolean) => void;
onConsumablesCostChange?: (cost: number) => void; onConsumablesCostChange?: (cost: number) => void;
onVolumeChange?: (totalVolume: number) => void; onVolumeChange?: (totalVolume: number) => void;
onSuppliersChange?: (suppliers: any[]) => void; onSuppliersChange?: (suppliers: unknown[]) => void;
} }
export function DirectSupplyCreation({ export function DirectSupplyCreation({
@ -888,12 +888,12 @@ export function DirectSupplyCreation({
// Проверяем есть ли уже выбранные поставщики и уведомляем родителя // Проверяем есть ли уже выбранные поставщики и уведомляем родителя
if (onSuppliersChange && supplyItems.length > 0) { if (onSuppliersChange && supplyItems.length > 0) {
const suppliersInfo = suppliersData.supplySuppliers.map((supplier: any) => ({ const suppliersInfo = suppliersData.supplySuppliers.map((supplier: { id: string; selected?: boolean }) => ({
...supplier, ...supplier,
selected: supplyItems.some(item => item.supplierId === supplier.id) 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); 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab"; import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
import { RealSupplyOrdersTab } from "./real-supply-orders-tab"; import { RealSupplyOrdersTab } from "./real-supply-orders-tab";
import { SellerSupplyOrdersTab } from "./seller-supply-orders-tab";
import { PvzReturnsTab } from "./pvz-returns-tab"; import { PvzReturnsTab } from "./pvz-returns-tab";
import { useAuth } from "@/hooks/useAuth";
interface FulfillmentSuppliesTabProps { interface FulfillmentSuppliesTabProps {
defaultSubTab?: string; defaultSubTab?: string;
@ -14,6 +16,7 @@ export function FulfillmentSuppliesTab({
defaultSubTab, defaultSubTab,
}: FulfillmentSuppliesTabProps) { }: FulfillmentSuppliesTabProps) {
const [activeSubTab, setActiveSubTab] = useState("goods"); const [activeSubTab, setActiveSubTab] = useState("goods");
const { user } = useAuth();
// Устанавливаем активную подвкладку при получении defaultSubTab // Устанавливаем активную подвкладку при получении defaultSubTab
useEffect(() => { useEffect(() => {
@ -22,6 +25,9 @@ export function FulfillmentSuppliesTab({
} }
}, [defaultSubTab]); }, [defaultSubTab]);
// Определяем тип организации для выбора правильного компонента
const isWholesale = user?.organization?.type === "WHOLESALE";
return ( return (
<div className="h-full overflow-hidden"> <div className="h-full overflow-hidden">
<Tabs <Tabs
@ -57,7 +63,7 @@ export function FulfillmentSuppliesTab({
</TabsContent> </TabsContent>
<TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden"> <TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden">
<RealSupplyOrdersTab /> {isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
</TabsContent> </TabsContent>
<TabsContent value="returns" className="mt-0 flex-1 overflow-hidden"> <TabsContent value="returns" className="mt-0 flex-1 overflow-hidden">

View File

@ -3,18 +3,28 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { StatsCard } from "../ui/stats-card"; import { StatsCard } from "../ui/stats-card";
import { StatsGrid } from "../ui/stats-grid"; 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 { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { import {
Calendar, Calendar,
MapPin,
Building2, Building2,
TrendingUp, TrendingUp,
DollarSign, DollarSign,
Wrench, Wrench,
Package2, Package2,
ChevronDown,
ChevronRight,
User,
CheckCircle,
XCircle,
Clock,
Truck,
} from "lucide-react"; } from "lucide-react";
interface SupplyOrderItem { interface SupplyOrderItem {
@ -36,10 +46,12 @@ interface SupplyOrderItem {
interface SupplyOrder { interface SupplyOrder {
id: string; id: string;
organizationId: string;
deliveryDate: string; deliveryDate: string;
status: string; status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
totalAmount: number; totalAmount: number;
totalItems: number; totalItems: number;
fulfillmentCenterId?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
partner: { partner: {
@ -57,15 +69,53 @@ interface SupplyOrder {
fullName?: string; fullName?: string;
type: string; type: string;
}; };
fulfillmentCenter?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
items: SupplyOrderItem[]; items: SupplyOrderItem[];
} }
export function RealSupplyOrdersTab() { export function RealSupplyOrdersTab() {
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set()); 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 toggleOrderExpansion = (orderId: string) => {
const newExpanded = new Set(expandedOrders); const newExpanded = new Set(expandedOrders);
@ -77,36 +127,59 @@ export function RealSupplyOrdersTab() {
setExpandedOrders(newExpanded); setExpandedOrders(newExpanded);
}; };
const getStatusBadge = (status: string) => { const handleStatusUpdate = async (
const statusMap: Record<string, { label: string; color: string }> = { orderId: string,
CREATED: { newStatus: SupplyOrder["status"]
label: "Создан", ) => {
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", color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
icon: Clock,
}, },
CONFIRMED: { CONFIRMED: {
label: "Подтвержден", label: "Одобрена",
color: "bg-green-500/20 text-green-300 border-green-500/30", color: "bg-green-500/20 text-green-300 border-green-500/30",
icon: CheckCircle,
}, },
IN_PROGRESS: { IN_TRANSIT: {
label: "В процессе", label: "В пути",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
icon: Truck,
}, },
DELIVERED: { DELIVERED: {
label: "Доставлен", label: "Доставлена",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30", color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
icon: Package2,
}, },
CANCELLED: { CANCELLED: {
label: "Отменен", label: "Отклонена",
color: "bg-red-500/20 text-red-300 border-red-500/30", color: "bg-red-500/20 text-red-300 border-red-500/30",
icon: XCircle,
}, },
}; };
const { label, color } = statusMap[status] || { const { label, color, icon: Icon } = statusMap[status];
label: status,
color: "bg-gray-500/20 text-gray-300 border-gray-500/30", return (
}; <Badge className={`${color} border flex items-center gap-1`}>
<Icon className="h-3 w-3" />
return <Badge className={`${color} border`}>{label}</Badge>; {label}
</Badge>
);
}; };
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
@ -135,17 +208,31 @@ export function RealSupplyOrdersTab() {
}); });
}; };
// Статистика // Статистика для оптовика
const totalOrders = supplyOrders.length; const totalOrders = incomingSupplyOrders.length;
const totalAmount = supplyOrders.reduce((sum, order) => sum + order.totalAmount, 0); const totalAmount = incomingSupplyOrders.reduce(
const totalItems = supplyOrders.reduce((sum, order) => sum + order.totalItems, 0); (sum, order) => sum + order.totalAmount,
const completedOrders = supplyOrders.filter(order => order.status === "DELIVERED").length; 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) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> <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> </div>
); );
} }
@ -155,7 +242,7 @@ export function RealSupplyOrdersTab() {
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-center"> <div className="text-center">
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" /> <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> <p className="text-white/60 text-sm mt-2">{error.message}</p>
</div> </div>
</div> </div>
@ -163,16 +250,43 @@ export function RealSupplyOrdersTab() {
} }
return ( return (
<div className="h-full flex flex-col space-y-6"> <div className="space-y-6">
{/* Статистика заказов расходников */} {/* Статистика входящих заявок */}
<StatsGrid> <StatsGrid>
<StatsCard <StatsCard
title="Всего заказов" title="Всего заявок"
value={totalOrders} value={totalOrders}
icon={Package2} icon={Package2}
iconColor="text-orange-400" iconColor="text-orange-400"
iconBg="bg-orange-500/20" 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 <StatsCard
@ -181,56 +295,47 @@ export function RealSupplyOrdersTab() {
icon={TrendingUp} icon={TrendingUp}
iconColor="text-green-400" iconColor="text-green-400"
iconBg="bg-green-500/20" iconBg="bg-green-500/20"
subtitle="Стоимость заказов" subtitle="Стоимость заявок"
/> />
<StatsCard <StatsCard
title="Всего единиц" title="Всего единиц"
value={totalItems} value={totalItems}
icon={Wrench} icon={Wrench}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
subtitle="Количество расходников"
/>
<StatsCard
title="Завершено"
value={completedOrders}
icon={Calendar}
iconColor="text-purple-400" iconColor="text-purple-400"
iconBg="bg-purple-500/20" iconBg="bg-purple-500/20"
subtitle="Доставленные заказы" subtitle="Количество товаров"
/> />
</StatsGrid> </StatsGrid>
{/* Список заказов расходников */} {/* Список входящих заявок */}
{supplyOrders.length === 0 ? ( {incomingSupplyOrders.length === 0 ? (
<Card className="bg-white/10 backdrop-blur border-white/20 p-8"> <Card className="bg-white/10 backdrop-blur border-white/20 p-8">
<div className="text-center"> <div className="text-center">
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" /> <Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2"> <h3 className="text-lg font-semibold text-white mb-2">
Пока нет заказов расходников Пока нет заявок
</h3> </h3>
<p className="text-white/60"> <p className="text-white/60">
Создайте первый заказ расходников через кнопку &quot;Создать поставку&quot; Здесь будут отображаться заявки от селлеров на поставку товаров
</p> </p>
</div> </div>
</Card> </Card>
) : ( ) : (
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden flex-1 flex flex-col"> <Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-auto flex-1"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-white/20"> <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">ID</th>
<th className="text-left p-4 text-white font-semibold"> <th className="text-left p-4 text-white font-semibold">
Поставщик Заказчик
</th> </th>
<th className="text-left p-4 text-white font-semibold"> <th className="text-left p-4 text-white font-semibold">
Дата поставки Дата поставки
</th> </th>
<th className="text-left p-4 text-white font-semibold"> <th className="text-left p-4 text-white font-semibold">
Дата создания Дата заявки
</th> </th>
<th className="text-left p-4 text-white font-semibold"> <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 className="text-left p-4 text-white font-semibold">
Статус Статус
</th> </th>
<th className="text-left p-4 text-white font-semibold">
Действия
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{supplyOrders.map((order) => { {incomingSupplyOrders.map((order) => {
const isOrderExpanded = expandedOrders.has(order.id); const isOrderExpanded = expandedOrders.has(order.id);
return ( return (
<React.Fragment key={order.id}> <React.Fragment key={order.id}>
{/* Основная строка заказа */} {/* Основная строка заказа */}
<tr <tr className="border-b border-white/10 hover:bg-white/5 transition-colors">
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer"
onClick={() => toggleOrderExpansion(order.id)}
>
<td className="p-4"> <td className="p-4">
<span className="text-white font-medium"> <div className="flex items-center space-x-2">
{order.id.slice(-8)} <button
</span> 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>
<td className="p-4"> <td className="p-4">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center space-x-2"> <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"> <span className="text-white font-medium">
{order.partner.name || order.partner.fullName || "Поставщик"} {order.organization.name ||
order.organization.fullName ||
"Заказчик"}
</span> </span>
</div> </div>
<p className="text-white/60 text-sm"> <p className="text-white/60 text-sm">
ИНН: {order.partner.inn} Тип: {order.organization.type}
</p> </p>
</div> </div>
</td> </td>
@ -299,16 +418,74 @@ export function RealSupplyOrdersTab() {
</div> </div>
</td> </td>
<td className="p-4">{getStatusBadge(order.status)}</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> </tr>
{/* Развернутая информация о заказе */} {/* Развернутая информация о заказе */}
{isOrderExpanded && ( {isOrderExpanded && (
<tr> <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="bg-white/5 border-t border-white/10">
<div className="p-6"> <div className="p-6">
<h4 className="text-white font-semibold mb-4"> <h4 className="text-white font-semibold mb-4">
Состав заказа: Состав заявки:
</h4> </h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{order.items.map((item) => ( {order.items.map((item) => (
@ -364,4 +541,4 @@ export function RealSupplyOrdersTab() {
)} )}
</div> </div>
); );
} }

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,98 +1,127 @@
"use client" "use client";
import React from 'react' import React from "react";
import { Package } from 'lucide-react' import { Package } from "lucide-react";
// Интерфейсы (можно будет вынести в отдельный файл types.ts) // Интерфейсы (можно будет вынести в отдельный файл types.ts)
interface WBStockInfo { interface WBStockInfo {
warehouseId: number warehouseId: number;
warehouseName: string warehouseName: string;
quantity: number quantity: number;
quantityFull: number quantityFull: number;
inWayToClient: number inWayToClient: number;
inWayFromClient: number inWayFromClient: number;
} }
interface WBStock { interface WBStock {
nmId: number nmId: number;
vendorCode: string vendorCode: string;
title: string title: string;
brand: string brand: string;
price: number price: number;
stocks: WBStockInfo[] stocks: WBStockInfo[];
totalQuantity: number totalQuantity: number;
totalReserved: number totalReserved: number;
photos: any[] photos: unknown[];
mediaFiles: any[] mediaFiles: unknown[];
characteristics: any[] characteristics: unknown[];
subjectName: string subjectName: string;
description: string description: string;
} }
interface StockTableRowProps { interface StockTableRowProps {
item: WBStock item: WBStock;
} }
export function StockTableRow({ item }: StockTableRowProps) { export function StockTableRow({ item }: StockTableRowProps) {
// Функция для получения изображений карточки // Функция для получения изображений карточки
const getCardImages = (item: WBStock) => { 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 // Проверяем photos
if (item.photos && item.photos.length > 0) { 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 // Проверяем mediaFiles
if (item.mediaFiles && item.mediaFiles.length > 0) { if (item.mediaFiles && item.mediaFiles.length > 0) {
return item.mediaFiles.map(media => media.big || media.c516x688 || media.c246x328 || media.square || media.tm || fallbackUrl) return item.mediaFiles.map(
(media) =>
media.big ||
media.c516x688 ||
media.c246x328 ||
media.square ||
media.tm ||
fallbackUrl
);
} }
return [fallbackUrl]
}
const getStockStatus = (quantity: number) => {
if (quantity === 0) return {
color: 'text-red-400',
bgColor: 'bg-red-500/10',
label: 'Нет в наличии'
}
if (quantity < 10) return {
color: 'text-orange-400',
bgColor: 'bg-orange-500/10',
label: 'Мало'
}
return {
color: 'text-green-400',
bgColor: 'bg-green-500/10',
label: 'В наличии'
}
}
const stockStatus = getStockStatus(item.totalQuantity) return [fallbackUrl];
const images = getCardImages(item) };
const mainImage = images[0] || null
const getStockStatus = (quantity: number) => {
if (quantity === 0)
return {
color: "text-red-400",
bgColor: "bg-red-500/10",
label: "Нет в наличии",
};
if (quantity < 10)
return {
color: "text-orange-400",
bgColor: "bg-orange-500/10",
label: "Мало",
};
return {
color: "text-green-400",
bgColor: "bg-green-500/10",
label: "В наличии",
};
};
const stockStatus = getStockStatus(item.totalQuantity);
const images = getCardImages(item);
const mainImage = images[0] || null;
// Отбираем ключевые характеристики для отображения в таблице // Отбираем ключевые характеристики для отображения в таблице
const keyCharacteristics = item.characteristics?.slice(0, 3) || [] const keyCharacteristics = item.characteristics?.slice(0, 3) || [];
return ( return (
<div className="group"> <div className="group">
{/* Основная строка товара */} {/* Основная строка товара */}
<div <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">
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 колонки) */} {/* Товар (3 колонки) */}
<div className="col-span-3 flex items-center gap-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"> <div className="w-12 h-12 rounded-lg overflow-hidden bg-white/10 flex-shrink-0">
{mainImage ? ( {mainImage ? (
<img <img
src={mainImage} src={mainImage}
alt={item.title} alt={item.title}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => { onError={(e) => {
const target = e.target as HTMLImageElement 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` 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-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <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"> <span className="text-xs text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded">
{item.brand || 'Без бренда'} {item.brand || "Без бренда"}
</span> </span>
<span className="text-white/40 text-xs">#{item.nmId}</span> <span className="text-white/40 text-xs">#{item.nmId}</span>
</div> </div>
<h3 className="text-white text-sm font-medium line-clamp-1 mb-1"> <h3 className="text-white text-sm font-medium line-clamp-1 mb-1">
{item.title} {item.title}
</h3> </h3>
<div className="text-white/60 text-xs"> <div className="text-white/60 text-xs">{item.vendorCode}</div>
{item.vendorCode}
</div>
</div> </div>
</div> </div>
@ -123,7 +150,9 @@ export function StockTableRow({ item }: StockTableRowProps) {
<div className={`text-lg font-bold ${stockStatus.color}`}> <div className={`text-lg font-bold ${stockStatus.color}`}>
{item.totalQuantity.toLocaleString()} {item.totalQuantity.toLocaleString()}
</div> </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} {stockStatus.label}
</div> </div>
</div> </div>
@ -164,16 +193,22 @@ export function StockTableRow({ item }: StockTableRowProps) {
<div className="space-y-1 w-full"> <div className="space-y-1 w-full">
{keyCharacteristics.map((char, index) => ( {keyCharacteristics.map((char, index) => (
<div key={index} className="flex justify-between text-xs"> <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"> <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> </span>
</div> </div>
))} ))}
{item.subjectName && ( {item.subjectName && (
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-white/60">Категория:</span> <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>
)} )}
</div> </div>
@ -184,38 +219,46 @@ export function StockTableRow({ item }: StockTableRowProps) {
<div className="grid grid-cols-12 gap-4 p-3 bg-white/[0.02] border-l-2 border-blue-400/30"> <div className="grid grid-cols-12 gap-4 p-3 bg-white/[0.02] border-l-2 border-blue-400/30">
<div className="col-span-12 flex flex-wrap gap-3"> <div className="col-span-12 flex flex-wrap gap-3">
{item.stocks.map((stock, stockIndex) => ( {item.stocks.map((stock, stockIndex) => (
<div <div
key={`${stock.warehouseId}-${stockIndex}`} key={`${stock.warehouseId}-${stockIndex}`}
className="bg-white/10 rounded-lg px-3 py-2 border border-white/20 hover:border-white/30 transition-colors" className="bg-white/10 rounded-lg px-3 py-2 border border-white/20 hover:border-white/30 transition-colors"
> >
{/* Название города */} {/* Название города */}
<div className="text-white text-sm font-medium mb-1"> <div className="text-white text-sm font-medium mb-1">
{stock.warehouseName} {stock.warehouseName}
</div> </div>
{/* Цифры */} {/* Цифры */}
<div className="flex items-center gap-3 text-xs"> <div className="flex items-center gap-3 text-xs">
<div className="text-center"> <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} {stock.quantity}
</div> </div>
<div className="text-white/50">остаток</div> <div className="text-white/50">остаток</div>
</div> </div>
{(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && ( {(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && (
<> <>
<div className="w-px h-6 bg-white/20" /> <div className="w-px h-6 bg-white/20" />
{stock.inWayToClient > 0 && ( {stock.inWayToClient > 0 && (
<div className="text-center"> <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 className="text-white/50">к клиенту</div>
</div> </div>
)} )}
{stock.inWayFromClient > 0 && ( {stock.inWayFromClient > 0 && (
<div className="text-center"> <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 className="text-white/50">от клиента</div>
</div> </div>
)} )}
@ -227,5 +270,5 @@ export function StockTableRow({ item }: StockTableRowProps) {
</div> </div>
</div> </div>
</div> </div>
) );
} }

View File

@ -1,261 +1,295 @@
"use client" "use client";
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from "react";
import { useAuth } from '@/hooks/useAuth' import { useAuth } from "@/hooks/useAuth";
import { Sidebar } from '@/components/dashboard/sidebar' import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from '@/hooks/useSidebar' import { useSidebar } from "@/hooks/useSidebar";
import { Card } from '@/components/ui/card' import { Card } from "@/components/ui/card";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { WildberriesService } from '@/services/wildberries-service' import { WildberriesService } from "@/services/wildberries-service";
import { toast } from 'sonner' import { toast } from "sonner";
import { StatsCards } from './stats-cards' import { StatsCards } from "./stats-cards";
import { SearchBar } from './search-bar' import { SearchBar } from "./search-bar";
import { TableHeader } from './table-header' import { TableHeader } from "./table-header";
import { LoadingSkeleton } from './loading-skeleton' import { LoadingSkeleton } from "./loading-skeleton";
import { StockTableRow } from './stock-table-row' import { StockTableRow } from "./stock-table-row";
import { TrendingUp, Package } from 'lucide-react' import { TrendingUp, Package } from "lucide-react";
interface WBStock { interface WBStock {
nmId: number nmId: number;
vendorCode: string vendorCode: string;
title: string title: string;
brand: string brand: string;
price: number price: number;
stocks: Array<{ stocks: Array<{
warehouseId: number warehouseId: number;
warehouseName: string warehouseName: string;
quantity: number quantity: number;
quantityFull: number quantityFull: number;
inWayToClient: number inWayToClient: number;
inWayFromClient: number inWayFromClient: number;
}> }>;
totalQuantity: number totalQuantity: number;
totalReserved: number totalReserved: number;
photos?: Array<{ photos?: Array<{
big?: string big?: string;
c246x328?: string c246x328?: string;
c516x688?: string c516x688?: string;
square?: string square?: string;
tm?: string tm?: string;
}> }>;
mediaFiles?: string[] mediaFiles?: string[];
characteristics?: Array<{ characteristics?: Array<{
name: string name: string;
value: string | string[] value: string | string[];
}> }>;
subjectName: string subjectName: string;
description: string description: string;
} }
interface WBWarehouse { interface WBWarehouse {
id: number id: number;
name: string name: string;
cargoType: number cargoType: number;
deliveryType: number deliveryType: number;
} }
export function WBWarehouseDashboard() { export function WBWarehouseDashboard() {
const { user } = useAuth() const { user } = useAuth();
const { isCollapsed, getSidebarMargin } = useSidebar() const { isCollapsed, getSidebarMargin } = useSidebar();
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)
// Analytics data
const [analyticsData, setAnalyticsData] = useState<any[]>([])
const hasWBApiKey = user?.wildberriesApiKey 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);
// Analytics data
const [analyticsData, setAnalyticsData] = useState<unknown[]>([]);
const hasWBApiKey = user?.wildberriesApiKey;
// Комбинирование карточек с индивидуальными данными аналитики // Комбинирование карточек с индивидуальными данными аналитики
// eslint-disable-next-line @typescript-eslint/no-explicit-any const combineCardsWithAnalytics = (
const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => { cards: unknown[],
const stocksMap = new Map<number, WBStock>() 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)
})
cards.forEach(card => { // Создаем карту аналитических данных для быстрого поиска
const analyticsMap = new Map(); // Map nmId to its analytics data
analyticsResults.forEach((result) => {
analyticsMap.set(result.nmId, result.data);
});
cards.forEach((card) => {
const stock: WBStock = { const stock: WBStock = {
nmId: card.nmID, nmId: card.nmID,
vendorCode: String(card.vendorCode || card.supplierVendorCode || ''), vendorCode: String(card.vendorCode || card.supplierVendorCode || ""),
title: String(card.title || card.object || `Товар ${card.nmID}`), title: String(card.title || card.object || `Товар ${card.nmID}`),
brand: String(card.brand || ''), brand: String(card.brand || ""),
price: 0, price: 0,
stocks: [], stocks: [],
totalQuantity: 0, totalQuantity: 0,
totalReserved: 0, totalReserved: 0,
photos: Array.isArray(card.photos) ? card.photos : [], photos: Array.isArray(card.photos) ? card.photos : [],
mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [], mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [],
characteristics: Array.isArray(card.characteristics) ? card.characteristics : [], characteristics: Array.isArray(card.characteristics)
subjectName: String(card.subjectName || card.object || ''), ? card.characteristics
description: String(card.description || '') : [],
} subjectName: String(card.subjectName || card.object || ""),
description: String(card.description || ""),
};
if (card.sizes && card.sizes.length > 0) { 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) { 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) { if (region.offices && region.offices.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any region.offices.forEach((office) => {
region.offices.forEach((office: any) => {
stock.stocks.push({ stock.stocks.push({
warehouseId: office.officeID, warehouseId: office.officeID,
warehouseName: office.officeName, warehouseName: office.officeName,
quantity: office.metrics?.stockCount || 0, quantity: office.metrics?.stockCount || 0,
quantityFull: office.metrics?.stockCount || 0, quantityFull: office.metrics?.stockCount || 0,
inWayToClient: office.metrics?.toClientCount || 0, inWayToClient: office.metrics?.toClientCount || 0,
inWayFromClient: office.metrics?.fromClientCount || 0 inWayFromClient: office.metrics?.fromClientCount || 0,
}) });
stock.totalQuantity += office.metrics?.stockCount || 0 stock.totalQuantity += office.metrics?.stockCount || 0;
stock.totalReserved += office.metrics?.toClientCount || 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 extractWarehousesFromStocks = (
const warehousesMap = new Map<number, WBWarehouse>() stocksData: WBStock[]
): WBWarehouse[] => {
stocksData.forEach(stock => { const warehousesMap = new Map<number, WBWarehouse>();
stock.stocks.forEach(stockInfo => {
stocksData.forEach((stock) => {
stock.stocks.forEach((stockInfo) => {
if (!warehousesMap.has(stockInfo.warehouseId)) { if (!warehousesMap.has(stockInfo.warehouseId)) {
warehousesMap.set(stockInfo.warehouseId, { warehousesMap.set(stockInfo.warehouseId, {
id: stockInfo.warehouseId, id: stockInfo.warehouseId,
name: stockInfo.warehouseName, name: stockInfo.warehouseName,
cargoType: 1, cargoType: 1,
deliveryType: 1 deliveryType: 1,
}) });
} }
}) });
}) });
return Array.from(warehousesMap.values()) return Array.from(warehousesMap.values());
} };
// Обновление статистики // Обновление статистики
const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => { const updateStatistics = (
setTotalProducts(stocksData.length) stocksData: WBStock[],
setTotalStocks(stocksData.reduce((sum, item) => sum + item.totalQuantity, 0)) warehousesData: WBWarehouse[]
setTotalReserved(stocksData.reduce((sum, item) => sum + item.totalReserved, 0)) ) => {
setTotalProducts(stocksData.length);
const totalFromClientCount = stocksData.reduce((sum, item) => setTotalStocks(
sum + item.stocks.reduce((stockSum, stock) => stockSum + stock.inWayFromClient, 0), 0 stocksData.reduce((sum, item) => sum + item.totalQuantity, 0)
) );
setTotalFromClient(totalFromClientCount) setTotalReserved(
stocksData.reduce((sum, item) => sum + item.totalReserved, 0)
);
const totalFromClientCount = stocksData.reduce(
(sum, item) =>
sum +
item.stocks.reduce(
(stockSum, stock) => stockSum + stock.inWayFromClient,
0
),
0
);
setTotalFromClient(totalFromClientCount);
const warehousesWithStock = new Set( const warehousesWithStock = new Set(
stocksData.flatMap(item => item.stocks.map(s => s.warehouseId)) stocksData.flatMap((item) => item.stocks.map((s) => s.warehouseId))
) );
setActiveWarehouses(warehousesWithStock.size) setActiveWarehouses(warehousesWithStock.size);
} };
// Загрузка данных склада // Загрузка данных склада
const loadWarehouseData = async () => { const loadWarehouseData = async () => {
if (!user?.wildberriesApiKey) return if (!user?.wildberriesApiKey) return;
setLoading(true) setLoading(true);
try { try {
const apiToken = user.wildberriesApiKey const apiToken = user.wildberriesApiKey;
const wbService = new WildberriesService(apiToken) const wbService = new WildberriesService(apiToken);
// 1. Получаем карточки товаров // 1. Получаем карточки товаров
const cards = await WildberriesService.getAllCards(apiToken).catch(() => []) const cards = await WildberriesService.getAllCards(apiToken).catch(
console.log('WB Warehouse: Loaded cards:', cards.length) () => []
);
console.log("WB Warehouse: Loaded cards:", cards.length);
if (cards.length === 0) { if (cards.length === 0) {
toast.error('Нет карточек товаров в WB') toast.error("Нет карточек товаров в WB");
return return;
} }
const nmIds = cards.map(card => card.nmID).filter(id => id > 0) const nmIds = cards.map((card) => card.nmID).filter((id) => id > 0);
console.log('WB Warehouse: NM IDs to process:', nmIds.length) console.log("WB Warehouse: NM IDs to process:", nmIds.length);
// 2. Получаем аналитику для каждого товара индивидуально // 2. Получаем аналитику для каждого товара индивидуально
const analyticsResults = [] const analyticsResults = [];
for (const nmId of nmIds) { for (const nmId of nmIds) {
try { try {
console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`) console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`);
const result = await wbService.getStocksReportByOffices({ const result = await wbService.getStocksReportByOffices({
nmIDs: [nmId], nmIDs: [nmId],
stockType: '' stockType: "",
}) });
analyticsResults.push({ nmId, data: result }) analyticsResults.push({ nmId, data: result });
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) { } 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. Комбинируем данные // 3. Комбинируем данные
const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults) const combinedStocks = combineCardsWithAnalytics(cards, analyticsResults);
console.log('WB Warehouse: Combined stocks:', combinedStocks.length) console.log("WB Warehouse: Combined stocks:", combinedStocks.length);
// 4. Извлекаем склады и обновляем статистику // 4. Извлекаем склады и обновляем статистику
const extractedWarehouses = extractWarehousesFromStocks(combinedStocks) const extractedWarehouses = extractWarehousesFromStocks(combinedStocks);
setStocks(combinedStocks)
setWarehouses(extractedWarehouses)
updateStatistics(combinedStocks, extractedWarehouses)
toast.success(`Загружено товаров: ${combinedStocks.length}`) setStocks(combinedStocks);
} catch (error: any) { setWarehouses(extractedWarehouses);
console.error('WB Warehouse: Error loading data:', error) updateStatistics(combinedStocks, extractedWarehouses);
toast.error('Ошибка загрузки данных: ' + (error.message || 'Неизвестная ошибка'))
toast.success(`Загружено товаров: ${combinedStocks.length}`);
} catch (error: unknown) {
console.error("Error loading warehouse data:", error);
toast.error("Ошибка при загрузке данных склада");
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
useEffect(() => { useEffect(() => {
if (hasWBApiKey) { if (hasWBApiKey) {
loadWarehouseData() loadWarehouseData();
} }
}, [hasWBApiKey]) }, [hasWBApiKey]);
// Фильтрация товаров // Фильтрация товаров
const filteredStocks = stocks.filter(item => { const filteredStocks = stocks.filter((item) => {
if (!searchTerm) return true if (!searchTerm) return true;
const search = searchTerm.toLowerCase() const search = searchTerm.toLowerCase();
return ( return (
item.title.toLowerCase().includes(search) || item.title.toLowerCase().includes(search) ||
String(item.nmId).includes(search) || String(item.nmId).includes(search) ||
item.brand.toLowerCase().includes(search) || item.brand.toLowerCase().includes(search) ||
item.vendorCode.toLowerCase().includes(search) item.vendorCode.toLowerCase().includes(search)
) );
}) });
return ( return (
<div className="h-screen flex overflow-hidden"> <div className="h-screen flex overflow-hidden">
<Sidebar /> <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"> <div className="h-full w-full flex flex-col">
{/* Результирующие вкладки */} {/* Результирующие вкладки */}
<StatsCards <StatsCards
totalProducts={totalProducts} totalProducts={totalProducts}
@ -275,16 +309,25 @@ export function WBWarehouseDashboard() {
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{analyticsData.map((warehouse) => ( {analyticsData.map((warehouse) => (
<Card key={warehouse.warehouseId} className="bg-white/5 border-white/10 p-3"> <Card
<div className="text-sm font-medium text-white mb-2">{warehouse.warehouseName}</div> 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="space-y-2">
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-white/60">К клиенту:</span> <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>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-white/60">От клиента:</span> <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>
</div> </div>
</Card> </Card>
@ -294,10 +337,7 @@ export function WBWarehouseDashboard() {
)} )}
{/* Поиск */} {/* Поиск */}
<SearchBar <SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
/>
{/* Список товаров */} {/* Список товаров */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
@ -309,10 +349,15 @@ export function WBWarehouseDashboard() {
) : !hasWBApiKey ? ( ) : !hasWBApiKey ? (
<Card className="glass-card border-white/10 p-8 text-center"> <Card className="glass-card border-white/10 p-8 text-center">
<Package className="h-12 w-12 text-blue-400 mx-auto mb-4" /> <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> <h3 className="text-lg font-medium text-white mb-2">
<p className="text-white/60 mb-4">Для просмотра остатков добавьте API ключ Wildberries в настройках</p> Настройте API Wildberries
<Button </h3>
onClick={() => window.location.href = '/settings'} <p className="text-white/60 mb-4">
Для просмотра остатков добавьте API ключ Wildberries в
настройках
</p>
<Button
onClick={() => (window.location.href = "/settings")}
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
> >
Перейти в настройки Перейти в настройки
@ -321,13 +366,17 @@ export function WBWarehouseDashboard() {
) : filteredStocks.length === 0 ? ( ) : filteredStocks.length === 0 ? (
<Card className="glass-card border-white/10 p-8 text-center"> <Card className="glass-card border-white/10 p-8 text-center">
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" /> <Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">Товары не найдены</h3> <h3 className="text-lg font-medium text-white mb-2">
<p className="text-white/60">Попробуйте изменить параметры поиска</p> Товары не найдены
</h3>
<p className="text-white/60">
Попробуйте изменить параметры поиска
</p>
</Card> </Card>
) : ( ) : (
<div className="overflow-y-auto pr-2 max-h-full"> <div className="overflow-y-auto pr-2 max-h-full">
<TableHeader /> <TableHeader />
{/* Таблица товаров */} {/* Таблица товаров */}
<div className="space-y-1"> <div className="space-y-1">
{filteredStocks.map((item, index) => ( {filteredStocks.map((item, index) => (
@ -340,5 +389,5 @@ export function WBWarehouseDashboard() {
</div> </div>
</main> </main>
</div> </div>
) );
} }

View File

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

View File

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