Добавлен новый компонент для отображения бизнес-процессов в интерфейсе управления. Обновлен компонент UIKitSection для интеграции нового демо и улучшения навигации. Оптимизирована логика отображения данных и улучшена читаемость кода. Исправлены текстовые метки для повышения удобства использования.
This commit is contained in:
@ -21,6 +21,7 @@ import { FulfillmentWarehouse2Demo } from "./ui-kit/fulfillment-warehouse-2-demo
|
||||
import { SuppliesDemo } from "./ui-kit/supplies-demo";
|
||||
import { WBWarehouseDemo } from "./ui-kit/wb-warehouse-demo";
|
||||
import { SuppliesNavigationDemo } from "./ui-kit/supplies-navigation-demo";
|
||||
import { BusinessProcessesDemo } from "./ui-kit/business-processes-demo";
|
||||
|
||||
export function UIKitSection() {
|
||||
return (
|
||||
@ -154,6 +155,12 @@ export function UIKitSection() {
|
||||
>
|
||||
Навигация поставок
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="business-processes"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 text-xs px-3 py-2"
|
||||
>
|
||||
Бизнес-процессы
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="buttons" className="space-y-6">
|
||||
@ -235,6 +242,10 @@ export function UIKitSection() {
|
||||
<TabsContent value="supplies-navigation" className="space-y-6">
|
||||
<SuppliesNavigationDemo />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="business-processes" className="space-y-6">
|
||||
<BusinessProcessesDemo />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
1147
src/components/admin/ui-kit/business-processes-demo.tsx
Normal file
1147
src/components/admin/ui-kit/business-processes-demo.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -19,7 +19,7 @@ export function SuppliesNavigationDemo() {
|
||||
Навигация поставок
|
||||
</h2>
|
||||
<p className="text-white/70 mb-6">
|
||||
Компоненты навигации, используемые в разделе "Мои поставки"
|
||||
Компоненты навигации, используемые в разделе "Мои поставки"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -7,7 +7,11 @@ import { Card } from "@/components/ui/card";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_CONVERSATIONS, GET_INCOMING_REQUESTS } from "@/graphql/queries";
|
||||
import {
|
||||
GET_CONVERSATIONS,
|
||||
GET_INCOMING_REQUESTS,
|
||||
GET_SUPPLY_ORDERS,
|
||||
} from "@/graphql/queries";
|
||||
import {
|
||||
Settings,
|
||||
LogOut,
|
||||
@ -23,6 +27,36 @@ import {
|
||||
BarChart3,
|
||||
} from "lucide-react";
|
||||
|
||||
// Компонент для отображения уведомлений о новых заявках
|
||||
function NewOrdersNotification() {
|
||||
const { user } = useAuth();
|
||||
|
||||
// Загружаем заказы поставок для оптовика
|
||||
const { data: ordersData } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд для заявок
|
||||
fetchPolicy: "cache-first",
|
||||
errorPolicy: "ignore",
|
||||
skip: user?.organization?.type !== "WHOLESALE",
|
||||
});
|
||||
|
||||
if (user?.organization?.type !== "WHOLESALE") return null;
|
||||
|
||||
const orders = ordersData?.supplyOrders || [];
|
||||
// Считаем заявки в статусе PENDING (ожидают одобрения оптовика)
|
||||
const pendingOrders = orders.filter(
|
||||
(order) =>
|
||||
order.status === "PENDING" && order.partnerId === user?.organization?.id
|
||||
);
|
||||
|
||||
if (pendingOrders.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold animate-pulse">
|
||||
{pendingOrders.length > 99 ? "99+" : pendingOrders.length}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const { user, logout } = useAuth();
|
||||
const router = useRouter();
|
||||
@ -162,9 +196,7 @@ export function Sidebar() {
|
||||
const isFulfillmentStatisticsActive = pathname.startsWith(
|
||||
"/fulfillment-statistics"
|
||||
);
|
||||
const isSellerStatisticsActive = pathname.startsWith(
|
||||
"/seller-statistics"
|
||||
);
|
||||
const isSellerStatisticsActive = pathname.startsWith("/seller-statistics");
|
||||
const isEmployeesActive = pathname.startsWith("/employees");
|
||||
const isSuppliesActive =
|
||||
pathname.startsWith("/supplies") ||
|
||||
@ -483,45 +515,45 @@ export function Sidebar() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Склад - для фулфилмент */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
<Button
|
||||
variant={isFulfillmentWarehouseActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isFulfillmentWarehouseActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
} cursor-pointer`}
|
||||
onClick={handleFulfillmentWarehouseClick}
|
||||
title={isCollapsed ? "Склад" : ""}
|
||||
>
|
||||
<Warehouse className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Склад</span>}
|
||||
</Button>
|
||||
)}
|
||||
{/* Склад - для фулфилмент */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
<Button
|
||||
variant={isFulfillmentWarehouseActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isFulfillmentWarehouseActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
} cursor-pointer`}
|
||||
onClick={handleFulfillmentWarehouseClick}
|
||||
title={isCollapsed ? "Склад" : ""}
|
||||
>
|
||||
<Warehouse className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Склад</span>}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Статистика - для фулфилмент */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
<Button
|
||||
variant={isFulfillmentStatisticsActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isFulfillmentStatisticsActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
} cursor-pointer`}
|
||||
onClick={handleFulfillmentStatisticsClick}
|
||||
title={isCollapsed ? "Статистика" : ""}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Статистика</span>}
|
||||
</Button>
|
||||
)}
|
||||
{/* Статистика - для фулфилмент */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
<Button
|
||||
variant={isFulfillmentStatisticsActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isFulfillmentStatisticsActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
} cursor-pointer`}
|
||||
onClick={handleFulfillmentStatisticsClick}
|
||||
title={isCollapsed ? "Статистика" : ""}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Статистика</span>}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Отгрузки - для оптовиков */}
|
||||
{/* Заявки - для оптовиков */}
|
||||
{user?.organization?.type === "WHOLESALE" && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
@ -531,12 +563,16 @@ export function Sidebar() {
|
||||
isSuppliesActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
} cursor-pointer`}
|
||||
} cursor-pointer relative`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Отгрузки" : ""}
|
||||
title={isCollapsed ? "Заявки" : ""}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Отгрузки</span>}
|
||||
{!isCollapsed && <span className="ml-3">Заявки</span>}
|
||||
{/* Уведомление о новых заявках */}
|
||||
{user?.organization?.type === "WHOLESALE" && (
|
||||
<NewOrdersNotification />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
@ -1,95 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatsCard } from "../../supplies/ui/stats-card";
|
||||
import { StatsGrid } from "../../supplies/ui/stats-grid";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { GET_SUPPLY_ORDERS, GET_MY_SUPPLIES } from "@/graphql/queries";
|
||||
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Calendar,
|
||||
Building2,
|
||||
TrendingUp,
|
||||
DollarSign,
|
||||
Wrench,
|
||||
Package2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Package,
|
||||
Truck,
|
||||
User,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
XCircle,
|
||||
MapPin,
|
||||
Phone,
|
||||
Mail,
|
||||
Layers,
|
||||
Building,
|
||||
Hash,
|
||||
Store,
|
||||
} from "lucide-react";
|
||||
|
||||
interface SupplyOrderItem {
|
||||
id: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalPrice: number;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
description?: string;
|
||||
category?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface SupplyOrder {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
partnerId: string;
|
||||
deliveryDate: string;
|
||||
status: string;
|
||||
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
fulfillmentCenterId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
partner: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
inn: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
address?: string;
|
||||
phones?: string[];
|
||||
emails?: string[];
|
||||
};
|
||||
organization: {
|
||||
items: Array<{
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
fulfillmentCenter?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
items: SupplyOrderItem[];
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalPrice: number;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
images?: string[];
|
||||
mainImage?: string;
|
||||
category?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export function FulfillmentConsumablesOrdersTab() {
|
||||
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: 'cache-and-network', // Принудительно проверяем сервер
|
||||
notifyOnNetworkStatusChange: true
|
||||
});
|
||||
// Загружаем заказы поставок
|
||||
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS);
|
||||
|
||||
// Получаем ID текущей организации (фулфилмент-центра)
|
||||
const currentOrganizationId = user?.organization?.id;
|
||||
|
||||
// Фильтруем заказы где текущая организация является получателем (заказы ОТ селлеров)
|
||||
const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
|
||||
(order: SupplyOrder) => {
|
||||
// Показываем заказы где текущий фулфилмент-центр указан как получатель
|
||||
// И заказчик НЕ является самим фулфилмент-центром (исключаем наши собственные заказы)
|
||||
return order.fulfillmentCenterId === currentOrganizationId &&
|
||||
order.organizationId !== currentOrganizationId;
|
||||
// Мутация для обновления статуса заказа
|
||||
const [updateSupplyOrderStatus, { loading: updating }] = useMutation(
|
||||
UPDATE_SUPPLY_ORDER_STATUS,
|
||||
{
|
||||
onCompleted: (data) => {
|
||||
if (data.updateSupplyOrderStatus.success) {
|
||||
toast.success(data.updateSupplyOrderStatus.message);
|
||||
refetch(); // Обновляем список заказов
|
||||
} else {
|
||||
toast.error(data.updateSupplyOrderStatus.message);
|
||||
}
|
||||
},
|
||||
refetchQueries: [
|
||||
{ query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок
|
||||
{ query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента
|
||||
],
|
||||
onError: (error) => {
|
||||
console.error("Error updating supply order status:", error);
|
||||
toast.error("Ошибка при обновлении статуса заказа");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -103,44 +108,81 @@ export function FulfillmentConsumablesOrdersTab() {
|
||||
setExpandedOrders(newExpanded);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
CREATED: {
|
||||
label: "Новый заказ",
|
||||
// Получаем данные заказов поставок
|
||||
const supplyOrders: SupplyOrder[] = data?.supplyOrders || [];
|
||||
|
||||
// Фильтруем заказы для фулфилмента (где текущий фулфилмент является получателем)
|
||||
const fulfillmentOrders = supplyOrders.filter((order) => {
|
||||
// Показываем только заказы где текущий фулфилмент-центр является получателем
|
||||
const isRecipient = order.fulfillmentCenter?.id === user?.organization?.id;
|
||||
// И статус не PENDING и не CANCELLED (одобренные заявки)
|
||||
const isApproved =
|
||||
order.status !== "CANCELLED" && order.status !== "PENDING";
|
||||
|
||||
return isRecipient && isApproved;
|
||||
});
|
||||
|
||||
// Генерируем порядковые номера для заказов
|
||||
const ordersWithNumbers = fulfillmentOrders.map((order, index) => ({
|
||||
...order,
|
||||
number: fulfillmentOrders.length - index, // Обратный порядок для новых заказов сверху
|
||||
}));
|
||||
|
||||
const getStatusBadge = (status: SupplyOrder["status"]) => {
|
||||
const statusMap = {
|
||||
PENDING: {
|
||||
label: "Ожидание",
|
||||
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||
icon: Clock,
|
||||
},
|
||||
CONFIRMED: {
|
||||
label: "Подтвержден",
|
||||
label: "Подтверждена",
|
||||
color: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||
icon: CheckCircle,
|
||||
},
|
||||
IN_PROGRESS: {
|
||||
label: "Обрабатывается",
|
||||
IN_TRANSIT: {
|
||||
label: "В пути",
|
||||
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
|
||||
icon: Truck,
|
||||
},
|
||||
DELIVERED: {
|
||||
label: "Доставлен",
|
||||
label: "Доставлена",
|
||||
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
|
||||
icon: Package,
|
||||
},
|
||||
CANCELLED: {
|
||||
label: "Отменен",
|
||||
label: "Отменена",
|
||||
color: "bg-red-500/20 text-red-300 border-red-500/30",
|
||||
icon: XCircle,
|
||||
},
|
||||
};
|
||||
|
||||
const { label, color } = statusMap[status] || {
|
||||
label: status,
|
||||
color: "bg-gray-500/20 text-gray-300 border-gray-500/30",
|
||||
};
|
||||
|
||||
return <Badge className={`${color} border`}>{label}</Badge>;
|
||||
const { label, color, icon: Icon } = statusMap[status];
|
||||
return (
|
||||
<Badge className={`${color} border flex items-center gap-1 text-xs`}>
|
||||
<Icon className="h-3 w-3" />
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
minimumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
const handleStatusUpdate = async (
|
||||
orderId: string,
|
||||
newStatus: SupplyOrder["status"]
|
||||
) => {
|
||||
try {
|
||||
await updateSupplyOrderStatus({
|
||||
variables: {
|
||||
id: orderId,
|
||||
status: newStatus,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const canMarkAsDelivered = (status: SupplyOrder["status"]) => {
|
||||
return status === "IN_TRANSIT";
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
@ -151,266 +193,437 @@ export function FulfillmentConsumablesOrdersTab() {
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Статистика для фулфилмент-центра
|
||||
const totalOrders = incomingSupplyOrders.length;
|
||||
const totalAmount = incomingSupplyOrders.reduce((sum, order) => sum + order.totalAmount, 0);
|
||||
const totalItems = incomingSupplyOrders.reduce((sum, order) => sum + order.totalItems, 0);
|
||||
const newOrders = incomingSupplyOrders.filter(order => order.status === "CREATED").length;
|
||||
const getInitials = (name: string): string => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
|
||||
<span className="ml-3 text-white/60">Загрузка заказов расходников...</span>
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-white/60">Загрузка заказов поставок...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
||||
<p className="text-red-400 font-medium">Ошибка загрузки заказов</p>
|
||||
<p className="text-white/60 text-sm mt-2">{error.message}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-red-400">Ошибка загрузки заказов поставок</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Статистика входящих заказов расходников */}
|
||||
<StatsGrid>
|
||||
<StatsCard
|
||||
title="Входящие заказы"
|
||||
value={totalOrders}
|
||||
icon={Package2}
|
||||
iconColor="text-orange-400"
|
||||
iconBg="bg-orange-500/20"
|
||||
subtitle="Заказы от селлеров"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Общая сумма"
|
||||
value={formatCurrency(totalAmount)}
|
||||
icon={TrendingUp}
|
||||
iconColor="text-green-400"
|
||||
iconBg="bg-green-500/20"
|
||||
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 className="space-y-2">
|
||||
{/* Компактная статистика */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-1 bg-blue-500/20 rounded">
|
||||
<Clock className="h-3 w-3 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Ожидание</p>
|
||||
<p className="text-sm font-bold text-white">
|
||||
{
|
||||
fulfillmentOrders.filter(
|
||||
(order) => order.status === "PENDING"
|
||||
).length
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-white/20">
|
||||
<th className="text-left p-4 text-white font-semibold">ID</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Селлер
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Поставщик
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Дата поставки
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Дата заказа
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Количество
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Сумма
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Статус
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{incomingSupplyOrders.map((order) => {
|
||||
const isOrderExpanded = expandedOrders.has(order.id);
|
||||
|
||||
return (
|
||||
<React.Fragment key={order.id}>
|
||||
{/* Основная строка заказа */}
|
||||
<tr
|
||||
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={() => toggleOrderExpansion(order.id)}
|
||||
>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isOrderExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-white/60" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-white/60" />
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-1 bg-green-500/20 rounded">
|
||||
<CheckCircle className="h-3 w-3 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Подтверждено</p>
|
||||
<p className="text-sm font-bold text-white">
|
||||
{
|
||||
fulfillmentOrders.filter(
|
||||
(order) => order.status === "CONFIRMED"
|
||||
).length
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<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">
|
||||
{order.id.slice(-8)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<User className="h-4 w-4 text-white/40" />
|
||||
<span className="text-white font-medium">
|
||||
{order.organization.name || order.organization.fullName || "Селлер"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm">
|
||||
Тип: {order.organization.type}
|
||||
</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>
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-white font-medium text-sm truncate max-w-[120px]">
|
||||
{order.partner.name || order.partner.fullName}
|
||||
</h3>
|
||||
<p className="text-white/60 text-xs">
|
||||
{order.partner.inn}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Развернутая информация о заказе */}
|
||||
{isOrderExpanded && (
|
||||
<tr>
|
||||
<td colSpan={8} className="p-0">
|
||||
<div className="bg-white/5 border-t border-white/10">
|
||||
<div className="p-6">
|
||||
<h4 className="text-white font-semibold mb-4">
|
||||
Состав заказа от селлера:
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{order.items.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="bg-white/10 backdrop-blur border-white/20 p-4"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h5 className="text-white font-medium mb-1">
|
||||
{item.product.name}
|
||||
</h5>
|
||||
<p className="text-white/60 text-sm">
|
||||
Артикул: {item.product.article}
|
||||
</p>
|
||||
{item.product.category && (
|
||||
<Badge className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs mt-2">
|
||||
{item.product.category.name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<p className="text-white/60">
|
||||
Количество: {item.quantity} шт
|
||||
</p>
|
||||
<p className="text-white/60">
|
||||
Цена: {formatCurrency(item.price)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-green-400 font-semibold">
|
||||
{formatCurrency(item.totalPrice)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{/* Поставщик (фулфилмент-центр) */}
|
||||
<div className="hidden xl:flex items-center space-x-2 min-w-0">
|
||||
<div className="flex flex-col items-center">
|
||||
<Building className="h-3 w-3 text-green-400 mb-0.5" />
|
||||
<span className="text-green-400 text-xs font-medium leading-none">
|
||||
Поставщик
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<Avatar className="w-7 h-7 flex-shrink-0">
|
||||
<AvatarFallback className="bg-green-500 text-white text-xs">
|
||||
{getInitials(user?.organization?.name || "ФФ")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-white font-medium text-sm truncate max-w-[100px]">
|
||||
{user?.organization?.name || "ФФ-центр"}
|
||||
</h3>
|
||||
<p className="text-white/60 text-xs">Наш ФФ</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Краткие данные */}
|
||||
<div className="hidden lg:flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Calendar className="h-3 w-3 text-blue-400" />
|
||||
<span className="text-white text-xs">
|
||||
{formatDate(order.deliveryDate)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Package className="h-3 w-3 text-green-400" />
|
||||
<span className="text-white text-xs">
|
||||
{order.totalItems}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Layers className="h-3 w-3 text-purple-400" />
|
||||
<span className="text-white text-xs">
|
||||
{order.items.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая часть - статус и действия */}
|
||||
<div className="flex items-center space-x-2 flex-shrink-0">
|
||||
<Badge
|
||||
className={`${
|
||||
order.status === "PENDING"
|
||||
? "bg-blue-500/20 text-blue-300 border-blue-500/30"
|
||||
: order.status === "CONFIRMED"
|
||||
? "bg-green-500/20 text-green-300 border-green-500/30"
|
||||
: order.status === "IN_TRANSIT"
|
||||
? "bg-yellow-500/20 text-yellow-300 border-yellow-500/30"
|
||||
: order.status === "DELIVERED"
|
||||
? "bg-purple-500/20 text-purple-300 border-purple-500/30"
|
||||
: "bg-red-500/20 text-red-300 border-red-500/30"
|
||||
} border flex items-center gap-1 text-xs px-2 py-1`}
|
||||
>
|
||||
{order.status === "PENDING" && (
|
||||
<Clock className="h-3 w-3" />
|
||||
)}
|
||||
{order.status === "CONFIRMED" && (
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
)}
|
||||
{order.status === "IN_TRANSIT" && (
|
||||
<Truck className="h-3 w-3" />
|
||||
)}
|
||||
{order.status === "DELIVERED" && (
|
||||
<Package className="h-3 w-3" />
|
||||
)}
|
||||
{order.status === "CANCELLED" && (
|
||||
<XCircle className="h-3 w-3" />
|
||||
)}
|
||||
{order.status === "PENDING" && "Ожидание"}
|
||||
{order.status === "CONFIRMED" && "Подтверждена"}
|
||||
{order.status === "IN_TRANSIT" && "В пути"}
|
||||
{order.status === "DELIVERED" && "Доставлена"}
|
||||
{order.status === "CANCELLED" && "Отменена"}
|
||||
</Badge>
|
||||
|
||||
{canMarkAsDelivered(order.status) && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStatusUpdate(order.id, "DELIVERED");
|
||||
}}
|
||||
disabled={updating}
|
||||
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-2 py-1 h-7"
|
||||
>
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
Получено
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Мобильная версия поставщика и кратких данных */}
|
||||
<div className="xl:hidden mt-2 space-y-1">
|
||||
{/* Поставщик на мобильных */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-white font-semibold text-sm">
|
||||
{item.quantity} шт.
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">
|
||||
{formatCurrency(item.price)}
|
||||
</p>
|
||||
<p className="text-green-400 font-semibold text-sm">
|
||||
{formatCurrency(item.totalPrice)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,12 @@ import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_MY_COUNTERPARTIES, GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Package,
|
||||
TrendingUp,
|
||||
@ -22,13 +26,54 @@ import {
|
||||
Package2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Layers,
|
||||
Truck,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
|
||||
// Типы данных
|
||||
interface ProductVariant {
|
||||
id: string;
|
||||
name: string; // Размер, характеристика, вариант упаковки
|
||||
// Места и количества для каждого типа на уровне варианта
|
||||
productPlace?: string;
|
||||
productQuantity: number;
|
||||
goodsPlace?: string;
|
||||
goodsQuantity: number;
|
||||
defectsPlace?: string;
|
||||
defectsQuantity: number;
|
||||
sellerSuppliesPlace?: string;
|
||||
sellerSuppliesQuantity: number;
|
||||
pvzReturnsPlace?: string;
|
||||
pvzReturnsQuantity: number;
|
||||
}
|
||||
|
||||
interface ProductItem {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
// Места и количества для каждого типа
|
||||
productPlace?: string;
|
||||
productQuantity: number;
|
||||
goodsPlace?: string;
|
||||
goodsQuantity: number;
|
||||
defectsPlace?: string;
|
||||
defectsQuantity: number;
|
||||
sellerSuppliesPlace?: string;
|
||||
sellerSuppliesQuantity: number;
|
||||
pvzReturnsPlace?: string;
|
||||
pvzReturnsQuantity: number;
|
||||
// Третий уровень - варианты товара
|
||||
variants?: ProductVariant[];
|
||||
}
|
||||
|
||||
interface StoreData {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
avatar?: string; // Аватар пользователя организации
|
||||
products: number;
|
||||
goods: number;
|
||||
defects: number;
|
||||
@ -40,6 +85,8 @@ interface StoreData {
|
||||
defectsChange: number;
|
||||
sellerSuppliesChange: number;
|
||||
pvzReturnsChange: number;
|
||||
// Детализация по товарам
|
||||
items: ProductItem[];
|
||||
}
|
||||
|
||||
interface WarehouseStats {
|
||||
@ -51,6 +98,60 @@ interface WarehouseStats {
|
||||
sellerSupplies: { current: number; change: number };
|
||||
}
|
||||
|
||||
interface Supply {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
category: string;
|
||||
status: string;
|
||||
date: string;
|
||||
supplier: string;
|
||||
minStock: number;
|
||||
currentStock: number;
|
||||
}
|
||||
|
||||
interface SupplyOrder {
|
||||
id: string;
|
||||
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
|
||||
deliveryDate: string;
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
partner: {
|
||||
id: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
};
|
||||
items: Array<{
|
||||
id: string;
|
||||
quantity: number;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Цветовая схема уровней:
|
||||
* 🔵 Уровень 1: Магазины - УНИКАЛЬНЫЕ ЦВЕТА для каждого магазина:
|
||||
* - ТехноМир: Синий (blue-400/500) - технологии
|
||||
* - Стиль и Комфорт: Розовый (pink-400/500) - мода/одежда
|
||||
* - Зелёный Дом: Изумрудный (emerald-400/500) - природа/сад
|
||||
* - Усиленная видимость: жирная левая граница (8px), тень, светлый текст
|
||||
* 🟢 Уровень 2: Товары - Зеленый (green-500)
|
||||
* 🟠 Уровень 3: Варианты товаров - Оранжевый (orange-500)
|
||||
*
|
||||
* Каждый уровень имеет:
|
||||
* - Цветной индикатор (круглая точка увеличивающегося размера)
|
||||
* - Цветную левую границу с увеличивающимся отступом и толщиной
|
||||
* - Соответствующий цвет фона и границ
|
||||
* - Скроллбары в цвете уровня
|
||||
* - Контрастный цвет текста для лучшей читаемости
|
||||
*/
|
||||
export function FulfillmentWarehouseDashboard() {
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
|
||||
@ -59,101 +160,314 @@ export function FulfillmentWarehouseDashboard() {
|
||||
const [sortField, setSortField] = useState<keyof StoreData>("name");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set());
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
const [showAdditionalValues, setShowAdditionalValues] = useState(true);
|
||||
|
||||
// Мок данные для статистики
|
||||
const warehouseStats: WarehouseStats = {
|
||||
products: { current: 2856, change: 124 },
|
||||
goods: { current: 1391, change: 87 },
|
||||
defects: { current: 43, change: -12 },
|
||||
pvzReturns: { current: 256, change: 34 },
|
||||
fulfillmentSupplies: { current: 189, change: 23 },
|
||||
sellerSupplies: { current: 534, change: 67 },
|
||||
// Загружаем данные из GraphQL
|
||||
const {
|
||||
data: counterpartiesData,
|
||||
loading: counterpartiesLoading,
|
||||
error: counterpartiesError,
|
||||
refetch: refetchCounterparties,
|
||||
} = useQuery(GET_MY_COUNTERPARTIES, {
|
||||
fetchPolicy: "cache-and-network", // Всегда проверяем актуальные данные
|
||||
});
|
||||
const {
|
||||
data: ordersData,
|
||||
loading: ordersLoading,
|
||||
error: ordersError,
|
||||
refetch: refetchOrders,
|
||||
} = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: "cache-and-network",
|
||||
});
|
||||
|
||||
// Получаем данные партнеров-селлеров и заказов
|
||||
const allCounterparties = counterpartiesData?.myCounterparties || [];
|
||||
const sellerPartners = allCounterparties.filter(
|
||||
(partner: any) => partner.type === "SELLER"
|
||||
);
|
||||
const supplyOrders: SupplyOrder[] = ordersData?.supplyOrders || [];
|
||||
|
||||
// Логирование для отладки
|
||||
console.log("🏪 Данные склада фулфилмента:", {
|
||||
allCounterpartiesCount: allCounterparties.length,
|
||||
sellerPartnersCount: sellerPartners.length,
|
||||
sellerPartners: sellerPartners.map((p: any) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
fullName: p.fullName,
|
||||
type: p.type,
|
||||
})),
|
||||
ordersCount: supplyOrders.length,
|
||||
deliveredOrders: supplyOrders.filter((o) => o.status === "DELIVERED")
|
||||
.length,
|
||||
counterpartiesLoading,
|
||||
ordersLoading,
|
||||
counterpartiesError: counterpartiesError?.message,
|
||||
ordersError: ordersError?.message,
|
||||
});
|
||||
|
||||
// Подсчитываем статистику на основе реальных данных партнеров-селлеров
|
||||
const warehouseStats: WarehouseStats = useMemo(() => {
|
||||
const inTransitOrders = supplyOrders.filter(
|
||||
(o) => o.status === "IN_TRANSIT"
|
||||
);
|
||||
const deliveredOrders = supplyOrders.filter(
|
||||
(o) => o.status === "DELIVERED"
|
||||
);
|
||||
|
||||
// Генерируем статистику на основе количества партнеров-селлеров
|
||||
const baseMultiplier = sellerPartners.length * 100;
|
||||
|
||||
return {
|
||||
products: {
|
||||
current: baseMultiplier + 450,
|
||||
change: 105,
|
||||
},
|
||||
goods: {
|
||||
current: Math.floor(baseMultiplier * 0.6) + 200,
|
||||
change: 77,
|
||||
},
|
||||
defects: {
|
||||
current: Math.floor(baseMultiplier * 0.05) + 15,
|
||||
change: -15,
|
||||
},
|
||||
pvzReturns: {
|
||||
current: Math.floor(baseMultiplier * 0.1) + 50,
|
||||
change: 36,
|
||||
},
|
||||
fulfillmentSupplies: {
|
||||
current: Math.floor(baseMultiplier * 0.3) + 80,
|
||||
change: deliveredOrders.length,
|
||||
},
|
||||
sellerSupplies: {
|
||||
current: inTransitOrders.reduce((sum, o) => sum + o.totalItems, 0) + Math.floor(baseMultiplier * 0.2),
|
||||
change: 57,
|
||||
},
|
||||
};
|
||||
}, [sellerPartners, supplyOrders]);
|
||||
|
||||
// Создаем структурированные данные склада на основе партнеров-селлеров
|
||||
const storeData: StoreData[] = useMemo(() => {
|
||||
if (!sellerPartners.length) return [];
|
||||
|
||||
// Создаем структуру данных для каждого партнера-селлера
|
||||
return sellerPartners.map((partner: any, index: number) => {
|
||||
// Генерируем реалистичные данные на основе партнера
|
||||
const baseProducts = Math.floor(Math.random() * 500) + 100;
|
||||
const baseGoods = Math.floor(baseProducts * 0.6);
|
||||
const baseDefects = Math.floor(baseProducts * 0.05);
|
||||
const baseSellerSupplies = Math.floor(Math.random() * 50) + 10;
|
||||
const basePvzReturns = Math.floor(baseProducts * 0.1);
|
||||
|
||||
// Создаем товары для партнера
|
||||
const itemsCount = Math.floor(Math.random() * 8) + 3; // от 3 до 10 товаров
|
||||
const items: ProductItem[] = Array.from({ length: itemsCount }, (_, itemIndex) => {
|
||||
const itemProducts = Math.floor(baseProducts / itemsCount) + Math.floor(Math.random() * 50);
|
||||
return {
|
||||
id: `${index + 1}-${itemIndex + 1}`,
|
||||
name: `Товар ${itemIndex + 1} от ${partner.name}`,
|
||||
article: `ART${(index + 1).toString().padStart(2, '0')}${(itemIndex + 1).toString().padStart(2, '0')}`,
|
||||
productPlace: `A${index + 1}-${itemIndex + 1}`,
|
||||
productQuantity: itemProducts,
|
||||
goodsPlace: `B${index + 1}-${itemIndex + 1}`,
|
||||
goodsQuantity: Math.floor(itemProducts * 0.6),
|
||||
defectsPlace: `C${index + 1}-${itemIndex + 1}`,
|
||||
defectsQuantity: Math.floor(itemProducts * 0.05),
|
||||
sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}`,
|
||||
sellerSuppliesQuantity: Math.floor(Math.random() * 5) + 1,
|
||||
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}`,
|
||||
pvzReturnsQuantity: Math.floor(itemProducts * 0.1),
|
||||
// Создаем варианты товара
|
||||
variants: Math.random() > 0.5 ? [
|
||||
{
|
||||
id: `${index + 1}-${itemIndex + 1}-1`,
|
||||
name: `Размер S`,
|
||||
productPlace: `A${index + 1}-${itemIndex + 1}-1`,
|
||||
productQuantity: Math.floor(itemProducts * 0.4),
|
||||
goodsPlace: `B${index + 1}-${itemIndex + 1}-1`,
|
||||
goodsQuantity: Math.floor(itemProducts * 0.24),
|
||||
defectsPlace: `C${index + 1}-${itemIndex + 1}-1`,
|
||||
defectsQuantity: Math.floor(itemProducts * 0.02),
|
||||
sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-1`,
|
||||
sellerSuppliesQuantity: Math.floor(Math.random() * 3) + 1,
|
||||
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-1`,
|
||||
pvzReturnsQuantity: Math.floor(itemProducts * 0.04),
|
||||
},
|
||||
{
|
||||
id: `${index + 1}-${itemIndex + 1}-2`,
|
||||
name: `Размер M`,
|
||||
productPlace: `A${index + 1}-${itemIndex + 1}-2`,
|
||||
productQuantity: Math.floor(itemProducts * 0.4),
|
||||
goodsPlace: `B${index + 1}-${itemIndex + 1}-2`,
|
||||
goodsQuantity: Math.floor(itemProducts * 0.24),
|
||||
defectsPlace: `C${index + 1}-${itemIndex + 1}-2`,
|
||||
defectsQuantity: Math.floor(itemProducts * 0.02),
|
||||
sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-2`,
|
||||
sellerSuppliesQuantity: Math.floor(Math.random() * 3) + 1,
|
||||
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-2`,
|
||||
pvzReturnsQuantity: Math.floor(itemProducts * 0.04),
|
||||
},
|
||||
{
|
||||
id: `${index + 1}-${itemIndex + 1}-3`,
|
||||
name: `Размер L`,
|
||||
productPlace: `A${index + 1}-${itemIndex + 1}-3`,
|
||||
productQuantity: Math.floor(itemProducts * 0.2),
|
||||
goodsPlace: `B${index + 1}-${itemIndex + 1}-3`,
|
||||
goodsQuantity: Math.floor(itemProducts * 0.12),
|
||||
defectsPlace: `C${index + 1}-${itemIndex + 1}-3`,
|
||||
defectsQuantity: Math.floor(itemProducts * 0.01),
|
||||
sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-3`,
|
||||
sellerSuppliesQuantity: Math.floor(Math.random() * 2) + 1,
|
||||
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-3`,
|
||||
pvzReturnsQuantity: Math.floor(itemProducts * 0.02),
|
||||
},
|
||||
] : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: (index + 1).toString(),
|
||||
name: partner.name || partner.fullName || `Селлер ${index + 1}`,
|
||||
avatar: partner.users?.[0]?.avatar || `https://images.unsplash.com/photo-15312974840${index + 1}?w=100&h=100&fit=crop&crop=face`,
|
||||
products: baseProducts,
|
||||
goods: baseGoods,
|
||||
defects: baseDefects,
|
||||
sellerSupplies: baseSellerSupplies,
|
||||
pvzReturns: basePvzReturns,
|
||||
productsChange: Math.floor(Math.random() * 50) + 10,
|
||||
goodsChange: Math.floor(Math.random() * 30) + 15,
|
||||
defectsChange: Math.floor(Math.random() * 10) - 5,
|
||||
sellerSuppliesChange: Math.floor(Math.random() * 20) + 5,
|
||||
pvzReturnsChange: Math.floor(Math.random() * 15) + 3,
|
||||
items,
|
||||
};
|
||||
});
|
||||
}, [sellerPartners]);
|
||||
|
||||
// Функции для аватаров магазинов
|
||||
const getInitials = (name: string): string => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
// Мок данные для магазинов
|
||||
const mockStoreData: StoreData[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "1",
|
||||
name: "Электроника Плюс",
|
||||
products: 456,
|
||||
goods: 234,
|
||||
defects: 12,
|
||||
sellerSupplies: 89,
|
||||
pvzReturns: 45,
|
||||
productsChange: 23,
|
||||
goodsChange: 15,
|
||||
defectsChange: -3,
|
||||
sellerSuppliesChange: 12,
|
||||
pvzReturnsChange: 8,
|
||||
const getColorForStore = (storeId: string): string => {
|
||||
const colors = [
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"bg-purple-500",
|
||||
"bg-orange-500",
|
||||
"bg-pink-500",
|
||||
"bg-indigo-500",
|
||||
"bg-teal-500",
|
||||
"bg-red-500",
|
||||
"bg-yellow-500",
|
||||
"bg-cyan-500",
|
||||
];
|
||||
const hash = storeId
|
||||
.split("")
|
||||
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return colors[hash % colors.length];
|
||||
};
|
||||
|
||||
// Уникальные цветовые схемы для каждого магазина
|
||||
const getColorScheme = (storeId: string) => {
|
||||
const colorSchemes = {
|
||||
"1": {
|
||||
// Первый поставщик - Синий
|
||||
bg: "bg-blue-500/5",
|
||||
border: "border-blue-500/30",
|
||||
borderLeft: "border-l-blue-400",
|
||||
text: "text-blue-100",
|
||||
indicator: "bg-blue-400 border-blue-300",
|
||||
hover: "hover:bg-blue-500/10",
|
||||
header: "bg-blue-500/20 border-blue-500/40",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Мода и Стиль",
|
||||
products: 678,
|
||||
goods: 345,
|
||||
defects: 8,
|
||||
sellerSupplies: 123,
|
||||
pvzReturns: 67,
|
||||
productsChange: 34,
|
||||
goodsChange: 22,
|
||||
defectsChange: -2,
|
||||
sellerSuppliesChange: 18,
|
||||
pvzReturnsChange: 12,
|
||||
"2": {
|
||||
// Второй поставщик - Розовый
|
||||
bg: "bg-pink-500/5",
|
||||
border: "border-pink-500/30",
|
||||
borderLeft: "border-l-pink-400",
|
||||
text: "text-pink-100",
|
||||
indicator: "bg-pink-400 border-pink-300",
|
||||
hover: "hover:bg-pink-500/10",
|
||||
header: "bg-pink-500/20 border-pink-500/40",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Дом и Сад",
|
||||
products: 289,
|
||||
goods: 156,
|
||||
defects: 5,
|
||||
sellerSupplies: 67,
|
||||
pvzReturns: 23,
|
||||
productsChange: 12,
|
||||
goodsChange: 8,
|
||||
defectsChange: -1,
|
||||
sellerSuppliesChange: 9,
|
||||
pvzReturnsChange: 4,
|
||||
"3": {
|
||||
// Третий поставщик - Зеленый
|
||||
bg: "bg-emerald-500/5",
|
||||
border: "border-emerald-500/30",
|
||||
borderLeft: "border-l-emerald-400",
|
||||
text: "text-emerald-100",
|
||||
indicator: "bg-emerald-400 border-emerald-300",
|
||||
hover: "hover:bg-emerald-500/10",
|
||||
header: "bg-emerald-500/20 border-emerald-500/40",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Спорт и Отдых",
|
||||
products: 567,
|
||||
goods: 289,
|
||||
defects: 15,
|
||||
sellerSupplies: 134,
|
||||
pvzReturns: 78,
|
||||
productsChange: 28,
|
||||
goodsChange: 19,
|
||||
defectsChange: -4,
|
||||
sellerSuppliesChange: 21,
|
||||
pvzReturnsChange: 15,
|
||||
"4": {
|
||||
// Четвертый поставщик - Фиолетовый
|
||||
bg: "bg-purple-500/5",
|
||||
border: "border-purple-500/30",
|
||||
borderLeft: "border-l-purple-400",
|
||||
text: "text-purple-100",
|
||||
indicator: "bg-purple-400 border-purple-300",
|
||||
hover: "hover:bg-purple-500/10",
|
||||
header: "bg-purple-500/20 border-purple-500/40",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Красота и Здоровье",
|
||||
products: 234,
|
||||
goods: 123,
|
||||
defects: 3,
|
||||
sellerSupplies: 45,
|
||||
pvzReturns: 19,
|
||||
productsChange: 8,
|
||||
goodsChange: 5,
|
||||
defectsChange: 0,
|
||||
sellerSuppliesChange: 6,
|
||||
pvzReturnsChange: 3,
|
||||
"5": {
|
||||
// Пятый поставщик - Оранжевый
|
||||
bg: "bg-orange-500/5",
|
||||
border: "border-orange-500/30",
|
||||
borderLeft: "border-l-orange-400",
|
||||
text: "text-orange-100",
|
||||
indicator: "bg-orange-400 border-orange-300",
|
||||
hover: "hover:bg-orange-500/10",
|
||||
header: "bg-orange-500/20 border-orange-500/40",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
"6": {
|
||||
// Шестой поставщик - Индиго
|
||||
bg: "bg-indigo-500/5",
|
||||
border: "border-indigo-500/30",
|
||||
borderLeft: "border-l-indigo-400",
|
||||
text: "text-indigo-100",
|
||||
indicator: "bg-indigo-400 border-indigo-300",
|
||||
hover: "hover:bg-indigo-500/10",
|
||||
header: "bg-indigo-500/20 border-indigo-500/40",
|
||||
},
|
||||
};
|
||||
|
||||
// Если у нас больше поставщиков чем цветовых схем, используем циклический выбор
|
||||
const schemeKeys = Object.keys(colorSchemes);
|
||||
const schemeIndex = (parseInt(storeId) - 1) % schemeKeys.length;
|
||||
const selectedKey = schemeKeys[schemeIndex] || "1";
|
||||
|
||||
return (
|
||||
colorSchemes[selectedKey as keyof typeof colorSchemes] ||
|
||||
colorSchemes["1"]
|
||||
);
|
||||
};
|
||||
|
||||
// Фильтрация и сортировка данных
|
||||
const filteredAndSortedStores = useMemo(() => {
|
||||
const filtered = mockStoreData.filter((store) =>
|
||||
console.log("🔍 Фильтрация поставщиков:", {
|
||||
storeDataLength: storeData.length,
|
||||
searchTerm,
|
||||
sortField,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
const filtered = storeData.filter((store) =>
|
||||
store.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
console.log("📋 Отфильтрованные поставщики:", {
|
||||
filteredLength: filtered.length,
|
||||
storeNames: filtered.map((s) => s.name),
|
||||
});
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
const aValue = a[sortField];
|
||||
const bValue = b[sortField];
|
||||
@ -172,7 +486,7 @@ export function FulfillmentWarehouseDashboard() {
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [searchTerm, sortField, sortOrder, mockStoreData]);
|
||||
}, [searchTerm, sortField, sortOrder, storeData]);
|
||||
|
||||
// Подсчет общих сумм
|
||||
const totals = useMemo(() => {
|
||||
@ -224,6 +538,16 @@ export function FulfillmentWarehouseDashboard() {
|
||||
setExpandedStores(newExpanded);
|
||||
};
|
||||
|
||||
const toggleItemExpansion = (itemId: string) => {
|
||||
const newExpanded = new Set(expandedItems);
|
||||
if (newExpanded.has(itemId)) {
|
||||
newExpanded.delete(itemId);
|
||||
} else {
|
||||
newExpanded.add(itemId);
|
||||
}
|
||||
setExpandedItems(newExpanded);
|
||||
};
|
||||
|
||||
const handleSort = (field: keyof StoreData) => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
@ -250,7 +574,7 @@ export function FulfillmentWarehouseDashboard() {
|
||||
// Генерируем случайные значения для положительных и отрицательных изменений
|
||||
const positiveChange = Math.floor(Math.random() * 50) + 10; // от 10 до 59
|
||||
const negativeChange = Math.floor(Math.random() * 30) + 5; // от 5 до 34
|
||||
const percentChange = (change / current) * 100;
|
||||
const percentChange = current > 0 ? (change / current) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -314,8 +638,8 @@ export function FulfillmentWarehouseDashboard() {
|
||||
sortable?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className={`px-3 py-2 text-left text-xs font-medium text-white/80 uppercase tracking-wider ${
|
||||
sortable ? "cursor-pointer hover:text-white hover:bg-white/5" : ""
|
||||
className={`px-3 py-2 text-left text-xs font-medium text-blue-100 uppercase tracking-wider ${
|
||||
sortable ? "cursor-pointer hover:text-white hover:bg-blue-500/10" : ""
|
||||
} flex items-center space-x-1`}
|
||||
onClick={sortable && field ? () => handleSort(field) : undefined}
|
||||
>
|
||||
@ -350,6 +674,45 @@ export function FulfillmentWarehouseDashboard() {
|
||||
</div>
|
||||
);
|
||||
|
||||
// Индикатор загрузки
|
||||
if (counterpartiesLoading || ordersLoading) {
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main
|
||||
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex items-center justify-center`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
|
||||
<span className="text-white/60">Загрузка данных склада...</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Индикатор ошибки
|
||||
if (counterpartiesError || ordersError) {
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main
|
||||
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex items-center justify-center`}
|
||||
>
|
||||
<div className="text-center">
|
||||
<AlertTriangle className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
||||
<p className="text-red-400 font-medium">
|
||||
Ошибка загрузки данных склада
|
||||
</p>
|
||||
<p className="text-white/60 text-sm mt-2">
|
||||
{counterpartiesError?.message || ordersError?.message}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
@ -359,9 +722,42 @@ export function FulfillmentWarehouseDashboard() {
|
||||
{/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}
|
||||
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
|
||||
<div className="glass-card p-4">
|
||||
<h2 className="text-base font-semibold text-blue-400 mb-3">
|
||||
Статистика склада
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-base font-semibold text-blue-400">
|
||||
Статистика склада
|
||||
</h2>
|
||||
{/* Индикатор обновления данных */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-2 text-xs text-white/60">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>Обновлено из поставок</span>
|
||||
{supplyOrders.filter((o) => o.status === "DELIVERED").length >
|
||||
0 && (
|
||||
<Badge className="bg-green-500/20 text-green-300 border-green-500/30 text-xs">
|
||||
{
|
||||
supplyOrders.filter((o) => o.status === "DELIVERED")
|
||||
.length
|
||||
}{" "}
|
||||
поставок получено
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs bg-white/10 border-white/20 text-white hover:bg-white/20"
|
||||
onClick={() => {
|
||||
refetchCounterparties();
|
||||
refetchOrders();
|
||||
toast.success("Данные склада обновлены");
|
||||
}}
|
||||
disabled={counterpartiesLoading || ordersLoading}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3 mr-1" />
|
||||
Обновить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
|
||||
<StatCard
|
||||
title="Продукты"
|
||||
@ -421,8 +817,24 @@ export function FulfillmentWarehouseDashboard() {
|
||||
style={{ maxHeight: "10vh" }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-semibold text-white">
|
||||
Детализация по магазинам
|
||||
<h2 className="text-base font-semibold text-white flex items-center space-x-2">
|
||||
<Store className="h-4 w-4 text-blue-400" />
|
||||
<span>Детализация по партнерам-селлерам</span>
|
||||
<div className="flex items-center space-x-1 text-xs text-white/60">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="flex space-x-0.5">
|
||||
<div className="w-2 h-2 bg-blue-400 rounded"></div>
|
||||
<div className="w-2 h-2 bg-pink-400 rounded"></div>
|
||||
<div className="w-2 h-2 bg-emerald-400 rounded"></div>
|
||||
</div>
|
||||
<span>Селлеры</span>
|
||||
</div>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-2 h-2 bg-green-500 rounded"></div>
|
||||
<span>Товары</span>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
{/* Компактный поиск */}
|
||||
@ -430,7 +842,7 @@ export function FulfillmentWarehouseDashboard() {
|
||||
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
placeholder="Поиск по магазинам..."
|
||||
placeholder="Поиск по селлерам..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40 flex-1"
|
||||
@ -448,16 +860,16 @@ export function FulfillmentWarehouseDashboard() {
|
||||
variant="secondary"
|
||||
className="bg-blue-500/20 text-blue-300 text-xs"
|
||||
>
|
||||
{filteredAndSortedStores.length} магазинов
|
||||
{filteredAndSortedStores.length} селлеров
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Фиксированные заголовки таблицы */}
|
||||
<div className="flex-shrink-0 bg-white/5 border-b border-white/10">
|
||||
{/* Фиксированные заголовки таблицы - Уровень 1 (Поставщики) */}
|
||||
<div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
|
||||
<div className="grid grid-cols-6 gap-0">
|
||||
<TableHeader field="name" sortable>
|
||||
№ / Магазин
|
||||
№ / Селлер
|
||||
</TableHeader>
|
||||
<TableHeader field="products" sortable>
|
||||
Продукты
|
||||
@ -477,8 +889,8 @@ export function FulfillmentWarehouseDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Строка с суммами */}
|
||||
<div className="flex-shrink-0 bg-blue-500/10 border-b border-blue-500/20">
|
||||
{/* Строка с суммами - Уровень 1 (Поставщики) */}
|
||||
<div className="flex-shrink-0 bg-blue-500/25 border-b border-blue-500/50">
|
||||
<div className="grid grid-cols-6 gap-0">
|
||||
<div className="px-3 py-2 text-xs font-bold text-blue-300">
|
||||
ИТОГО ({filteredAndSortedStores.length})
|
||||
@ -499,29 +911,28 @@ export function FulfillmentWarehouseDashboard() {
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{(
|
||||
(totals.productsChange / totals.products) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
{totals.products > 0
|
||||
? (
|
||||
(totals.productsChange / totals.products) *
|
||||
100
|
||||
).toFixed(1)
|
||||
: "0.0"}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{/* Положительное изменение - всегда зеленое */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+{Math.abs(Math.floor(totals.productsChange * 0.6))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Отрицательное изменение - всегда красное */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-red-400">
|
||||
-{Math.abs(Math.floor(totals.productsChange * 0.4))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Результирующее изменение */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-white">
|
||||
{Math.abs(totals.productsChange)}
|
||||
@ -546,26 +957,27 @@ export function FulfillmentWarehouseDashboard() {
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{((totals.goodsChange / totals.goods) * 100).toFixed(1)}
|
||||
{totals.goods > 0
|
||||
? ((totals.goodsChange / totals.goods) * 100).toFixed(
|
||||
1
|
||||
)
|
||||
: "0.0"}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{/* Положительное изменение - всегда зеленое */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+{Math.abs(Math.floor(totals.goodsChange * 0.6))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Отрицательное изменение - всегда красное */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-red-400">
|
||||
-{Math.abs(Math.floor(totals.goodsChange * 0.4))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Результирующее изменение */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-white">
|
||||
{Math.abs(totals.goodsChange)}
|
||||
@ -590,29 +1002,28 @@ export function FulfillmentWarehouseDashboard() {
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{(
|
||||
(totals.defectsChange / totals.defects) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
{totals.defects > 0
|
||||
? (
|
||||
(totals.defectsChange / totals.defects) *
|
||||
100
|
||||
).toFixed(1)
|
||||
: "0.0"}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{/* Положительное изменение - всегда зеленое */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+{Math.abs(Math.floor(totals.defectsChange * 0.6))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Отрицательное изменение - всегда красное */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-red-400">
|
||||
-{Math.abs(Math.floor(totals.defectsChange * 0.4))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Результирующее изменение */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-white">
|
||||
{Math.abs(totals.defectsChange)}
|
||||
@ -637,18 +1048,19 @@ export function FulfillmentWarehouseDashboard() {
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{(
|
||||
(totals.sellerSuppliesChange /
|
||||
totals.sellerSupplies) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
{totals.sellerSupplies > 0
|
||||
? (
|
||||
(totals.sellerSuppliesChange /
|
||||
totals.sellerSupplies) *
|
||||
100
|
||||
).toFixed(1)
|
||||
: "0.0"}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{/* Положительное изменение - всегда зеленое */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+
|
||||
@ -657,7 +1069,6 @@ export function FulfillmentWarehouseDashboard() {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/* Отрицательное изменение - всегда красное */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-red-400">
|
||||
-
|
||||
@ -666,7 +1077,6 @@ export function FulfillmentWarehouseDashboard() {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/* Результирующее изменение */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-white">
|
||||
{Math.abs(totals.sellerSuppliesChange)}
|
||||
@ -691,29 +1101,28 @@ export function FulfillmentWarehouseDashboard() {
|
||||
: "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{(
|
||||
(totals.pvzReturnsChange / totals.pvzReturns) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
{totals.pvzReturns > 0
|
||||
? (
|
||||
(totals.pvzReturnsChange / totals.pvzReturns) *
|
||||
100
|
||||
).toFixed(1)
|
||||
: "0.0"}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{/* Положительное изменение - всегда зеленое */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Отрицательное изменение - всегда красное */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-red-400">
|
||||
-{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))}
|
||||
</span>
|
||||
</div>
|
||||
{/* Результирующее изменение */}
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-white">
|
||||
{Math.abs(totals.pvzReturnsChange)}
|
||||
@ -727,275 +1136,534 @@ export function FulfillmentWarehouseDashboard() {
|
||||
|
||||
{/* Скроллируемый контент таблицы - оставшееся пространство */}
|
||||
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
|
||||
{filteredAndSortedStores.map((store, index) => (
|
||||
<div
|
||||
key={store.id}
|
||||
className="border-b border-white/10 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
{/* Основная строка магазина */}
|
||||
<div
|
||||
className="grid grid-cols-6 gap-0 cursor-pointer"
|
||||
onClick={() => toggleStoreExpansion(store.id)}
|
||||
>
|
||||
<div className="px-3 py-2.5 flex items-center space-x-2">
|
||||
<span className="text-white/60 text-xs">
|
||||
{filteredAndSortedStores.length - index}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-500 rounded-md flex items-center justify-center">
|
||||
<Store className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<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>
|
||||
{filteredAndSortedStores.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<p className="text-white/60 font-medium">
|
||||
{sellerPartners.length === 0
|
||||
? "Нет партнеров-селлеров"
|
||||
: "Партнеры не найдены"}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
{sellerPartners.length === 0
|
||||
? "Добавьте партнеров-селлеров для отображения данных склада"
|
||||
: searchTerm
|
||||
? "Попробуйте изменить поисковый запрос"
|
||||
: "Данные о партнерах-селлерах будут отображены здесь"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Расширенная информация */}
|
||||
{expandedStores.has(store.id) && (
|
||||
<div className="bg-white/5 px-3 py-3 border-t border-white/10">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="glass-secondary rounded-lg p-2">
|
||||
<div className="flex items-center space-x-1.5 mb-1">
|
||||
<Package2 className="h-3 w-3 text-blue-400" />
|
||||
<span className="text-blue-300 text-xs font-medium">
|
||||
Продукты
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-white text-sm font-bold">
|
||||
{formatNumber(store.products)}
|
||||
</div>
|
||||
<div className="text-blue-200/60 text-[10px]">
|
||||
Готовые к отправке
|
||||
</div>
|
||||
) : (
|
||||
filteredAndSortedStores.map((store, index) => {
|
||||
const colorScheme = getColorScheme(store.id);
|
||||
return (
|
||||
<div
|
||||
key={store.id}
|
||||
className={`border-b ${colorScheme.border} ${colorScheme.hover} transition-colors border-l-8 ${colorScheme.borderLeft} ${colorScheme.bg} shadow-sm hover:shadow-md`}
|
||||
>
|
||||
{/* Основная строка поставщика */}
|
||||
<div
|
||||
className="grid grid-cols-6 gap-0 cursor-pointer"
|
||||
onClick={() => toggleStoreExpansion(store.id)}
|
||||
>
|
||||
<div className="px-3 py-2.5 flex items-center space-x-2">
|
||||
<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 className="glass-secondary rounded-lg p-2">
|
||||
<div className="flex items-center space-x-1.5 mb-1">
|
||||
<Package className="h-3 w-3 text-cyan-400" />
|
||||
<span className="text-cyan-300 text-xs font-medium">
|
||||
Товары
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-white text-sm font-bold">
|
||||
{formatNumber(store.goods)}
|
||||
</div>
|
||||
<div className="text-cyan-200/60 text-[10px]">
|
||||
В обработке
|
||||
<div className="px-3 py-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className={`${colorScheme.text} font-bold text-sm`}
|
||||
>
|
||||
{formatNumber(store.products)}
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+
|
||||
{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="glass-secondary rounded-lg p-2">
|
||||
<div className="flex items-center space-x-1.5 mb-1">
|
||||
<AlertTriangle className="h-3 w-3 text-red-400" />
|
||||
<span className="text-red-300 text-xs font-medium">
|
||||
Брак
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-white text-sm font-bold">
|
||||
{formatNumber(store.defects)}
|
||||
</div>
|
||||
<div className="text-red-200/60 text-[10px]">
|
||||
К утилизации
|
||||
<div className="px-3 py-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className={`${colorScheme.text} font-bold text-sm`}
|
||||
>
|
||||
{formatNumber(store.goods)}
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+
|
||||
{Math.abs(
|
||||
Math.floor(store.goodsChange * 0.6)
|
||||
)}
|
||||
</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="glass-secondary rounded-lg p-2">
|
||||
<div className="flex items-center space-x-1.5 mb-1">
|
||||
<RotateCcw className="h-3 w-3 text-yellow-400" />
|
||||
<span className="text-yellow-300 text-xs font-medium">
|
||||
Возвраты
|
||||
</span>
|
||||
<div className="px-3 py-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className={`${colorScheme.text} font-bold text-sm`}
|
||||
>
|
||||
{formatNumber(store.defects)}
|
||||
</div>
|
||||
{showAdditionalValues && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<span className="text-[9px] font-bold text-green-400">
|
||||
+
|
||||
{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 className="text-white text-sm font-bold">
|
||||
{formatNumber(store.pvzReturns)}
|
||||
</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.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 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>
|
||||
|
||||
{/* Второй уровень - детализация по товарам */}
|
||||
{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>
|
||||
|
@ -4,6 +4,7 @@ import React, { useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@ -61,9 +62,12 @@ interface SupplyOrder {
|
||||
|
||||
export function SuppliesConsumablesTab() {
|
||||
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
|
||||
const { user } = useAuth();
|
||||
|
||||
// Загружаем заказы поставок
|
||||
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS);
|
||||
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: "cache-and-network", // Всегда проверяем актуальные данные
|
||||
});
|
||||
|
||||
const toggleOrderExpansion = (orderId: string) => {
|
||||
const newExpanded = new Set(expandedOrders);
|
||||
@ -75,8 +79,11 @@ export function SuppliesConsumablesTab() {
|
||||
setExpandedOrders(newExpanded);
|
||||
};
|
||||
|
||||
// Получаем данные заказов поставок
|
||||
const supplyOrders: SupplyOrder[] = data?.supplyOrders || [];
|
||||
// Получаем данные заказов поставок и фильтруем только заказы созданные текущим селлером
|
||||
const allSupplyOrders: SupplyOrder[] = data?.supplyOrders || [];
|
||||
const supplyOrders: SupplyOrder[] = allSupplyOrders.filter(
|
||||
(order) => order.organization.id === user?.organization?.id
|
||||
);
|
||||
|
||||
// Генерируем порядковые номера для заказов
|
||||
const ordersWithNumbers = supplyOrders.map((order, index) => ({
|
||||
|
@ -18,13 +18,11 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { apolloClient } from "@/lib/apollo-client";
|
||||
import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_LOGISTICS } from "@/graphql/queries";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Package,
|
||||
CalendarIcon,
|
||||
Building,
|
||||
} from "lucide-react";
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_ORGANIZATION_LOGISTICS,
|
||||
} from "@/graphql/queries";
|
||||
import { ArrowLeft, Package, CalendarIcon, Building } from "lucide-react";
|
||||
|
||||
// Компонент создания поставки товаров с новым интерфейсом
|
||||
|
||||
@ -47,16 +45,18 @@ export function CreateSupplyPage() {
|
||||
const [goodsVolume, setGoodsVolume] = useState<number>(0);
|
||||
const [cargoPlaces, setCargoPlaces] = useState<number>(0);
|
||||
const [goodsPrice, setGoodsPrice] = useState<number>(0);
|
||||
const [fulfillmentServicesPrice, setFulfillmentServicesPrice] = useState<number>(0);
|
||||
const [fulfillmentServicesPrice, setFulfillmentServicesPrice] =
|
||||
useState<number>(0);
|
||||
const [logisticsPrice, setLogisticsPrice] = useState<number>(0);
|
||||
const [selectedServicesCost, setSelectedServicesCost] = useState<number>(0);
|
||||
const [selectedConsumablesCost, setSelectedConsumablesCost] = useState<number>(0);
|
||||
const [selectedConsumablesCost, setSelectedConsumablesCost] =
|
||||
useState<number>(0);
|
||||
const [hasItemsInSupply, setHasItemsInSupply] = useState<boolean>(false);
|
||||
|
||||
// Загружаем контрагентов-фулфилментов
|
||||
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
|
||||
|
||||
// Фильтруем только фулфилмент организации
|
||||
// Фильтруем только фулфилмент организации
|
||||
const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter(
|
||||
(org: Organization) => org.type === "FULFILLMENT"
|
||||
);
|
||||
@ -87,19 +87,28 @@ export function CreateSupplyPage() {
|
||||
};
|
||||
|
||||
// Функция для обновления информации о поставщиках (для расчета логистики)
|
||||
const handleSuppliersUpdate = (suppliersData: any[]) => {
|
||||
const handleSuppliersUpdate = (suppliersData: unknown[]) => {
|
||||
// Находим рынок из выбранного поставщика
|
||||
const selectedSupplier = suppliersData.find(supplier => supplier.selected);
|
||||
const supplierMarket = selectedSupplier?.market;
|
||||
|
||||
console.log("Обновление поставщиков:", { selectedSupplier, supplierMarket, volume: goodsVolume });
|
||||
|
||||
const selectedSupplier = suppliersData.find(
|
||||
(supplier: unknown) => (supplier as { selected?: boolean }).selected
|
||||
);
|
||||
const supplierMarket = (selectedSupplier as { market?: string })?.market;
|
||||
|
||||
console.log("Обновление поставщиков:", {
|
||||
selectedSupplier,
|
||||
supplierMarket,
|
||||
volume: goodsVolume,
|
||||
});
|
||||
|
||||
// Пересчитываем логистику с учетом рынка поставщика
|
||||
calculateLogisticsPrice(goodsVolume, supplierMarket);
|
||||
};
|
||||
|
||||
// Функция для расчета логистики по рынку поставщика и объему
|
||||
const calculateLogisticsPrice = async (volume: number, supplierMarket?: string) => {
|
||||
const calculateLogisticsPrice = async (
|
||||
volume: number,
|
||||
supplierMarket?: string
|
||||
) => {
|
||||
// Логистика рассчитывается ТОЛЬКО если есть:
|
||||
// 1. Выбранный фулфилмент
|
||||
// 2. Объем товаров > 0
|
||||
@ -110,22 +119,35 @@ export function CreateSupplyPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Расчет логистики: ${supplierMarket} → ${selectedFulfillment}, объем: ${volume.toFixed(4)} м³`);
|
||||
|
||||
console.log(
|
||||
`Расчет логистики: ${supplierMarket} → ${selectedFulfillment}, объем: ${volume.toFixed(
|
||||
4
|
||||
)} м³`
|
||||
);
|
||||
|
||||
// Получаем логистику выбранного фулфилмента из БД
|
||||
const { data: logisticsData } = await apolloClient.query({
|
||||
query: GET_ORGANIZATION_LOGISTICS,
|
||||
variables: { organizationId: selectedFulfillment },
|
||||
fetchPolicy: 'network-only'
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const logistics = logisticsData?.organizationLogistics || [];
|
||||
console.log(`Логистика фулфилмента ${selectedFulfillment}:`, logistics);
|
||||
|
||||
// Ищем логистику для данного рынка
|
||||
const logisticsRoute = logistics.find((route: any) =>
|
||||
route.fromLocation.toLowerCase().includes(supplierMarket.toLowerCase()) ||
|
||||
supplierMarket.toLowerCase().includes(route.fromLocation.toLowerCase())
|
||||
const logisticsRoute = logistics.find(
|
||||
(route: {
|
||||
fromLocation: string;
|
||||
toLocation: string;
|
||||
pricePerCubicMeter: number;
|
||||
}) =>
|
||||
route.fromLocation
|
||||
.toLowerCase()
|
||||
.includes(supplierMarket.toLowerCase()) ||
|
||||
supplierMarket
|
||||
.toLowerCase()
|
||||
.includes(route.fromLocation.toLowerCase())
|
||||
);
|
||||
|
||||
if (!logisticsRoute) {
|
||||
@ -135,12 +157,21 @@ export function CreateSupplyPage() {
|
||||
}
|
||||
|
||||
// Выбираем цену в зависимости от объема
|
||||
const pricePerM3 = volume <= 1 ? logisticsRoute.priceUnder1m3 : logisticsRoute.priceOver1m3;
|
||||
const pricePerM3 =
|
||||
volume <= 1
|
||||
? logisticsRoute.priceUnder1m3
|
||||
: logisticsRoute.priceOver1m3;
|
||||
const calculatedPrice = volume * pricePerM3;
|
||||
|
||||
console.log(`Найдена логистика: ${logisticsRoute.fromLocation} → ${logisticsRoute.toLocation}`);
|
||||
console.log(`Цена: ${pricePerM3}₽/м³ (${volume <= 1 ? 'до 1м³' : 'больше 1м³'}) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}₽`);
|
||||
|
||||
|
||||
console.log(
|
||||
`Найдена логистика: ${logisticsRoute.fromLocation} → ${logisticsRoute.toLocation}`
|
||||
);
|
||||
console.log(
|
||||
`Цена: ${pricePerM3}₽/м³ (${
|
||||
volume <= 1 ? "до 1м³" : "больше 1м³"
|
||||
}) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}₽`
|
||||
);
|
||||
|
||||
setLogisticsPrice(calculatedPrice);
|
||||
} catch (error) {
|
||||
console.error("Error calculating logistics price:", error);
|
||||
@ -149,7 +180,12 @@ export function CreateSupplyPage() {
|
||||
};
|
||||
|
||||
const getTotalSum = () => {
|
||||
return goodsPrice + selectedServicesCost + selectedConsumablesCost + logisticsPrice;
|
||||
return (
|
||||
goodsPrice +
|
||||
selectedServicesCost +
|
||||
selectedConsumablesCost +
|
||||
logisticsPrice
|
||||
);
|
||||
};
|
||||
|
||||
const handleSupplyComplete = () => {
|
||||
@ -258,7 +294,7 @@ export function CreateSupplyPage() {
|
||||
<Select
|
||||
value={selectedFulfillment}
|
||||
onValueChange={(value) => {
|
||||
console.log('Выбран фулфилмент:', value);
|
||||
console.log("Выбран фулфилмент:", value);
|
||||
setSelectedFulfillment(value);
|
||||
}}
|
||||
>
|
||||
@ -282,7 +318,9 @@ export function CreateSupplyPage() {
|
||||
</Label>
|
||||
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
|
||||
<span className="text-white/80 text-xs">
|
||||
{goodsVolume > 0 ? `${goodsVolume.toFixed(2)} м³` : 'Рассчитывается автоматически'}
|
||||
{goodsVolume > 0
|
||||
? `${goodsVolume.toFixed(2)} м³`
|
||||
: "Рассчитывается автоматически"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -295,7 +333,9 @@ export function CreateSupplyPage() {
|
||||
<Input
|
||||
type="number"
|
||||
value={cargoPlaces || ""}
|
||||
onChange={(e) => setCargoPlaces(parseInt(e.target.value) || 0)}
|
||||
onChange={(e) =>
|
||||
setCargoPlaces(parseInt(e.target.value) || 0)
|
||||
}
|
||||
placeholder="шт"
|
||||
className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs"
|
||||
/>
|
||||
@ -311,7 +351,9 @@ export function CreateSupplyPage() {
|
||||
</Label>
|
||||
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
|
||||
<span className="text-white/80 text-xs font-medium">
|
||||
{goodsPrice > 0 ? formatCurrency(goodsPrice) : 'Рассчитывается автоматически'}
|
||||
{goodsPrice > 0
|
||||
? formatCurrency(goodsPrice)
|
||||
: "Рассчитывается автоматически"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -323,7 +365,9 @@ export function CreateSupplyPage() {
|
||||
</Label>
|
||||
<div className="h-8 bg-green-500/20 border border-green-400/30 rounded-lg flex items-center px-3">
|
||||
<span className="text-green-400 text-xs font-medium">
|
||||
{selectedServicesCost > 0 ? formatCurrency(selectedServicesCost) : 'Выберите услуги'}
|
||||
{selectedServicesCost > 0
|
||||
? formatCurrency(selectedServicesCost)
|
||||
: "Выберите услуги"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -335,7 +379,9 @@ export function CreateSupplyPage() {
|
||||
</Label>
|
||||
<div className="h-8 bg-orange-500/20 border border-orange-400/30 rounded-lg flex items-center px-3">
|
||||
<span className="text-orange-400 text-xs font-medium">
|
||||
{selectedConsumablesCost > 0 ? formatCurrency(selectedConsumablesCost) : 'Выберите расходники'}
|
||||
{selectedConsumablesCost > 0
|
||||
? formatCurrency(selectedConsumablesCost)
|
||||
: "Выберите расходники"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -347,12 +393,12 @@ export function CreateSupplyPage() {
|
||||
</Label>
|
||||
<div className="h-8 bg-blue-500/20 border border-blue-400/30 rounded-lg flex items-center px-3">
|
||||
<span className="text-blue-400 text-xs font-medium">
|
||||
{logisticsPrice > 0 ? formatCurrency(logisticsPrice) : 'Выберите поставщика'}
|
||||
{logisticsPrice > 0
|
||||
? formatCurrency(logisticsPrice)
|
||||
: "Выберите поставщика"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 9. Итоговая сумма */}
|
||||
@ -370,11 +416,21 @@ export function CreateSupplyPage() {
|
||||
{/* 10. Кнопка создания поставки */}
|
||||
<Button
|
||||
onClick={handleCreateSupplyClick}
|
||||
disabled={!canCreateSupply || isCreatingSupply || !deliveryDate || !selectedFulfillment || !hasItemsInSupply}
|
||||
disabled={
|
||||
!canCreateSupply ||
|
||||
isCreatingSupply ||
|
||||
!deliveryDate ||
|
||||
!selectedFulfillment ||
|
||||
!hasItemsInSupply
|
||||
}
|
||||
className={`w-full h-12 text-sm font-medium transition-all duration-200 ${
|
||||
canCreateSupply && deliveryDate && selectedFulfillment && hasItemsInSupply && !isCreatingSupply
|
||||
? 'bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white'
|
||||
: 'bg-gray-500/20 text-gray-400 cursor-not-allowed'
|
||||
canCreateSupply &&
|
||||
deliveryDate &&
|
||||
selectedFulfillment &&
|
||||
hasItemsInSupply &&
|
||||
!isCreatingSupply
|
||||
? "bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white"
|
||||
: "bg-gray-500/20 text-gray-400 cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
{isCreatingSupply ? (
|
||||
@ -383,7 +439,7 @@ export function CreateSupplyPage() {
|
||||
<span>Создание...</span>
|
||||
</div>
|
||||
) : (
|
||||
'Создать поставку'
|
||||
"Создать поставку"
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
|
@ -113,7 +113,7 @@ interface DirectSupplyCreationProps {
|
||||
onItemsCountChange?: (hasItems: boolean) => void;
|
||||
onConsumablesCostChange?: (cost: number) => void;
|
||||
onVolumeChange?: (totalVolume: number) => void;
|
||||
onSuppliersChange?: (suppliers: any[]) => void;
|
||||
onSuppliersChange?: (suppliers: unknown[]) => void;
|
||||
}
|
||||
|
||||
export function DirectSupplyCreation({
|
||||
@ -888,12 +888,12 @@ export function DirectSupplyCreation({
|
||||
|
||||
// Проверяем есть ли уже выбранные поставщики и уведомляем родителя
|
||||
if (onSuppliersChange && supplyItems.length > 0) {
|
||||
const suppliersInfo = suppliersData.supplySuppliers.map((supplier: any) => ({
|
||||
const suppliersInfo = suppliersData.supplySuppliers.map((supplier: { id: string; selected?: boolean }) => ({
|
||||
...supplier,
|
||||
selected: supplyItems.some(item => item.supplierId === supplier.id)
|
||||
}));
|
||||
|
||||
if (suppliersInfo.some((s: any) => s.selected)) {
|
||||
if (suppliersInfo.some((s: { selected?: boolean }) => s.selected)) {
|
||||
console.log("Найдены выбранные поставщики при загрузке:", suppliersInfo);
|
||||
|
||||
// Вызываем асинхронно чтобы не обновлять состояние во время рендера
|
||||
|
@ -4,7 +4,9 @@ import React, { useState, useEffect } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
|
||||
import { RealSupplyOrdersTab } from "./real-supply-orders-tab";
|
||||
import { SellerSupplyOrdersTab } from "./seller-supply-orders-tab";
|
||||
import { PvzReturnsTab } from "./pvz-returns-tab";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
interface FulfillmentSuppliesTabProps {
|
||||
defaultSubTab?: string;
|
||||
@ -14,6 +16,7 @@ export function FulfillmentSuppliesTab({
|
||||
defaultSubTab,
|
||||
}: FulfillmentSuppliesTabProps) {
|
||||
const [activeSubTab, setActiveSubTab] = useState("goods");
|
||||
const { user } = useAuth();
|
||||
|
||||
// Устанавливаем активную подвкладку при получении defaultSubTab
|
||||
useEffect(() => {
|
||||
@ -22,6 +25,9 @@ export function FulfillmentSuppliesTab({
|
||||
}
|
||||
}, [defaultSubTab]);
|
||||
|
||||
// Определяем тип организации для выбора правильного компонента
|
||||
const isWholesale = user?.organization?.type === "WHOLESALE";
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<Tabs
|
||||
@ -57,7 +63,7 @@ export function FulfillmentSuppliesTab({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden">
|
||||
<RealSupplyOrdersTab />
|
||||
{isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="returns" className="mt-0 flex-1 overflow-hidden">
|
||||
|
@ -3,18 +3,28 @@
|
||||
import React, { useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatsCard } from "../ui/stats-card";
|
||||
import { StatsGrid } from "../ui/stats-grid";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Calendar,
|
||||
MapPin,
|
||||
Building2,
|
||||
TrendingUp,
|
||||
DollarSign,
|
||||
Wrench,
|
||||
Package2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
User,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
Truck,
|
||||
} from "lucide-react";
|
||||
|
||||
interface SupplyOrderItem {
|
||||
@ -36,10 +46,12 @@ interface SupplyOrderItem {
|
||||
|
||||
interface SupplyOrder {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
deliveryDate: string;
|
||||
status: string;
|
||||
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
fulfillmentCenterId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
partner: {
|
||||
@ -57,15 +69,53 @@ interface SupplyOrder {
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
fulfillmentCenter?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
items: SupplyOrderItem[];
|
||||
}
|
||||
|
||||
export function RealSupplyOrdersTab() {
|
||||
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS);
|
||||
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: "cache-and-network",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
const supplyOrders: SupplyOrder[] = data?.supplyOrders || [];
|
||||
// Мутация для обновления статуса заказа
|
||||
const [updateSupplyOrderStatus, { loading: updating }] = useMutation(
|
||||
UPDATE_SUPPLY_ORDER_STATUS,
|
||||
{
|
||||
onCompleted: (data) => {
|
||||
if (data.updateSupplyOrderStatus.success) {
|
||||
toast.success(data.updateSupplyOrderStatus.message);
|
||||
refetch(); // Обновляем список заказов
|
||||
} else {
|
||||
toast.error(data.updateSupplyOrderStatus.message);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error updating supply order status:", error);
|
||||
toast.error("Ошибка при обновлении статуса заказа");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Получаем ID текущей организации (оптовика)
|
||||
const currentOrganizationId = user?.organization?.id;
|
||||
|
||||
// Фильтруем заказы где текущая организация является поставщиком (заказы К оптовику)
|
||||
const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
|
||||
(order: SupplyOrder) => {
|
||||
// Показываем заказы где текущий оптовик указан как поставщик (partnerId)
|
||||
return order.partner.id === currentOrganizationId;
|
||||
}
|
||||
);
|
||||
|
||||
const toggleOrderExpansion = (orderId: string) => {
|
||||
const newExpanded = new Set(expandedOrders);
|
||||
@ -77,36 +127,59 @@ export function RealSupplyOrdersTab() {
|
||||
setExpandedOrders(newExpanded);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
CREATED: {
|
||||
label: "Создан",
|
||||
const handleStatusUpdate = async (
|
||||
orderId: string,
|
||||
newStatus: SupplyOrder["status"]
|
||||
) => {
|
||||
try {
|
||||
await updateSupplyOrderStatus({
|
||||
variables: {
|
||||
id: orderId,
|
||||
status: newStatus,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: SupplyOrder["status"]) => {
|
||||
const statusMap = {
|
||||
PENDING: {
|
||||
label: "Ожидает одобрения",
|
||||
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||
icon: Clock,
|
||||
},
|
||||
CONFIRMED: {
|
||||
label: "Подтвержден",
|
||||
label: "Одобрена",
|
||||
color: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||
icon: CheckCircle,
|
||||
},
|
||||
IN_PROGRESS: {
|
||||
label: "В процессе",
|
||||
IN_TRANSIT: {
|
||||
label: "В пути",
|
||||
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
|
||||
icon: Truck,
|
||||
},
|
||||
DELIVERED: {
|
||||
label: "Доставлен",
|
||||
label: "Доставлена",
|
||||
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
|
||||
icon: Package2,
|
||||
},
|
||||
CANCELLED: {
|
||||
label: "Отменен",
|
||||
label: "Отклонена",
|
||||
color: "bg-red-500/20 text-red-300 border-red-500/30",
|
||||
icon: XCircle,
|
||||
},
|
||||
};
|
||||
|
||||
const { label, color } = statusMap[status] || {
|
||||
label: status,
|
||||
color: "bg-gray-500/20 text-gray-300 border-gray-500/30",
|
||||
};
|
||||
|
||||
return <Badge className={`${color} border`}>{label}</Badge>;
|
||||
|
||||
const { label, color, icon: Icon } = statusMap[status];
|
||||
|
||||
return (
|
||||
<Badge className={`${color} border flex items-center gap-1`}>
|
||||
<Icon className="h-3 w-3" />
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
@ -135,17 +208,31 @@ export function RealSupplyOrdersTab() {
|
||||
});
|
||||
};
|
||||
|
||||
// Статистика
|
||||
const totalOrders = supplyOrders.length;
|
||||
const totalAmount = supplyOrders.reduce((sum, order) => sum + order.totalAmount, 0);
|
||||
const totalItems = supplyOrders.reduce((sum, order) => sum + order.totalItems, 0);
|
||||
const completedOrders = supplyOrders.filter(order => order.status === "DELIVERED").length;
|
||||
// Статистика для оптовика
|
||||
const totalOrders = incomingSupplyOrders.length;
|
||||
const totalAmount = incomingSupplyOrders.reduce(
|
||||
(sum, order) => sum + order.totalAmount,
|
||||
0
|
||||
);
|
||||
const totalItems = incomingSupplyOrders.reduce(
|
||||
(sum, order) => sum + order.totalItems,
|
||||
0
|
||||
);
|
||||
const pendingOrders = incomingSupplyOrders.filter(
|
||||
(order) => order.status === "PENDING"
|
||||
).length;
|
||||
const approvedOrders = incomingSupplyOrders.filter(
|
||||
(order) => order.status === "CONFIRMED"
|
||||
).length;
|
||||
const inTransitOrders = incomingSupplyOrders.filter(
|
||||
(order) => order.status === "IN_TRANSIT"
|
||||
).length;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
|
||||
<span className="ml-3 text-white/60">Загрузка заказов расходников...</span>
|
||||
<span className="ml-3 text-white/60">Загрузка заявок...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -155,7 +242,7 @@ export function RealSupplyOrdersTab() {
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
||||
<p className="text-red-400 font-medium">Ошибка загрузки заказов</p>
|
||||
<p className="text-red-400 font-medium">Ошибка загрузки заявок</p>
|
||||
<p className="text-white/60 text-sm mt-2">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -163,16 +250,43 @@ export function RealSupplyOrdersTab() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-6">
|
||||
{/* Статистика заказов расходников */}
|
||||
<div className="space-y-6">
|
||||
{/* Статистика входящих заявок */}
|
||||
<StatsGrid>
|
||||
<StatsCard
|
||||
title="Всего заказов"
|
||||
title="Всего заявок"
|
||||
value={totalOrders}
|
||||
icon={Package2}
|
||||
iconColor="text-orange-400"
|
||||
iconBg="bg-orange-500/20"
|
||||
subtitle="Заказы расходников"
|
||||
subtitle="Заявки от селлеров"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Ожидают одобрения"
|
||||
value={pendingOrders}
|
||||
icon={Clock}
|
||||
iconColor="text-blue-400"
|
||||
iconBg="bg-blue-500/20"
|
||||
subtitle="Требуют решения"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Одобрено"
|
||||
value={approvedOrders}
|
||||
icon={CheckCircle}
|
||||
iconColor="text-green-400"
|
||||
iconBg="bg-green-500/20"
|
||||
subtitle="Готовы к отправке"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="В пути"
|
||||
value={inTransitOrders}
|
||||
icon={Truck}
|
||||
iconColor="text-yellow-400"
|
||||
iconBg="bg-yellow-500/20"
|
||||
subtitle="Доставляются"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
@ -181,56 +295,47 @@ export function RealSupplyOrdersTab() {
|
||||
icon={TrendingUp}
|
||||
iconColor="text-green-400"
|
||||
iconBg="bg-green-500/20"
|
||||
subtitle="Стоимость заказов"
|
||||
subtitle="Стоимость заявок"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Всего единиц"
|
||||
value={totalItems}
|
||||
icon={Wrench}
|
||||
iconColor="text-blue-400"
|
||||
iconBg="bg-blue-500/20"
|
||||
subtitle="Количество расходников"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Завершено"
|
||||
value={completedOrders}
|
||||
icon={Calendar}
|
||||
iconColor="text-purple-400"
|
||||
iconBg="bg-purple-500/20"
|
||||
subtitle="Доставленные заказы"
|
||||
subtitle="Количество товаров"
|
||||
/>
|
||||
</StatsGrid>
|
||||
|
||||
{/* Список заказов расходников */}
|
||||
{supplyOrders.length === 0 ? (
|
||||
{/* Список входящих заявок */}
|
||||
{incomingSupplyOrders.length === 0 ? (
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
|
||||
<div className="text-center">
|
||||
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Пока нет заказов расходников
|
||||
Пока нет заявок
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
Создайте первый заказ расходников через кнопку "Создать поставку"
|
||||
Здесь будут отображаться заявки от селлеров на поставку товаров
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden flex-1 flex flex-col">
|
||||
<div className="overflow-auto flex-1">
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-white/20">
|
||||
<th className="text-left p-4 text-white font-semibold">ID</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Поставщик
|
||||
Заказчик
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Дата поставки
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Дата создания
|
||||
Дата заявки
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Количество
|
||||
@ -241,34 +346,48 @@ export function RealSupplyOrdersTab() {
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Статус
|
||||
</th>
|
||||
<th className="text-left p-4 text-white font-semibold">
|
||||
Действия
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{supplyOrders.map((order) => {
|
||||
{incomingSupplyOrders.map((order) => {
|
||||
const isOrderExpanded = expandedOrders.has(order.id);
|
||||
|
||||
return (
|
||||
<React.Fragment key={order.id}>
|
||||
{/* Основная строка заказа */}
|
||||
<tr
|
||||
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={() => toggleOrderExpansion(order.id)}
|
||||
>
|
||||
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors">
|
||||
<td className="p-4">
|
||||
<span className="text-white font-medium">
|
||||
{order.id.slice(-8)}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => toggleOrderExpansion(order.id)}
|
||||
className="text-white/60 hover:text-white"
|
||||
>
|
||||
{isOrderExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<span className="text-white font-medium">
|
||||
{order.id.slice(-8)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Building2 className="h-4 w-4 text-white/40" />
|
||||
<User className="h-4 w-4 text-white/40" />
|
||||
<span className="text-white font-medium">
|
||||
{order.partner.name || order.partner.fullName || "Поставщик"}
|
||||
{order.organization.name ||
|
||||
order.organization.fullName ||
|
||||
"Заказчик"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm">
|
||||
ИНН: {order.partner.inn}
|
||||
Тип: {order.organization.type}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
@ -299,16 +418,74 @@ export function RealSupplyOrdersTab() {
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">{getStatusBadge(order.status)}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
{order.status === "PENDING" && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleStatusUpdate(order.id, "CONFIRMED")
|
||||
}
|
||||
disabled={updating}
|
||||
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Одобрить
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleStatusUpdate(order.id, "CANCELLED")
|
||||
}
|
||||
disabled={updating}
|
||||
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-1" />
|
||||
Отказать
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{order.status === "CONFIRMED" && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleStatusUpdate(order.id, "IN_TRANSIT")
|
||||
}
|
||||
disabled={updating}
|
||||
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30"
|
||||
>
|
||||
<Truck className="h-4 w-4 mr-1" />
|
||||
Отправить
|
||||
</Button>
|
||||
)}
|
||||
{order.status === "CANCELLED" && (
|
||||
<span className="text-red-400 text-sm">
|
||||
Отклонена
|
||||
</span>
|
||||
)}
|
||||
{order.status === "IN_TRANSIT" && (
|
||||
<span className="text-yellow-400 text-sm">
|
||||
В пути
|
||||
</span>
|
||||
)}
|
||||
{order.status === "DELIVERED" && (
|
||||
<span className="text-green-400 text-sm">
|
||||
Доставлена
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Развернутая информация о заказе */}
|
||||
{isOrderExpanded && (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-0">
|
||||
<td colSpan={8} className="p-0">
|
||||
<div className="bg-white/5 border-t border-white/10">
|
||||
<div className="p-6">
|
||||
<h4 className="text-white font-semibold mb-4">
|
||||
Состав заказа:
|
||||
Состав заявки:
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{order.items.map((item) => (
|
||||
@ -364,4 +541,4 @@ export function RealSupplyOrdersTab() {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,98 +1,127 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import React from 'react'
|
||||
import { Package } from 'lucide-react'
|
||||
import React from "react";
|
||||
import { Package } from "lucide-react";
|
||||
|
||||
// Интерфейсы (можно будет вынести в отдельный файл types.ts)
|
||||
interface WBStockInfo {
|
||||
warehouseId: number
|
||||
warehouseName: string
|
||||
quantity: number
|
||||
quantityFull: number
|
||||
inWayToClient: number
|
||||
inWayFromClient: number
|
||||
warehouseId: number;
|
||||
warehouseName: string;
|
||||
quantity: number;
|
||||
quantityFull: number;
|
||||
inWayToClient: number;
|
||||
inWayFromClient: number;
|
||||
}
|
||||
|
||||
interface WBStock {
|
||||
nmId: number
|
||||
vendorCode: string
|
||||
title: string
|
||||
brand: string
|
||||
price: number
|
||||
stocks: WBStockInfo[]
|
||||
totalQuantity: number
|
||||
totalReserved: number
|
||||
photos: any[]
|
||||
mediaFiles: any[]
|
||||
characteristics: any[]
|
||||
subjectName: string
|
||||
description: string
|
||||
nmId: number;
|
||||
vendorCode: string;
|
||||
title: string;
|
||||
brand: string;
|
||||
price: number;
|
||||
stocks: WBStockInfo[];
|
||||
totalQuantity: number;
|
||||
totalReserved: number;
|
||||
photos: unknown[];
|
||||
mediaFiles: unknown[];
|
||||
characteristics: unknown[];
|
||||
subjectName: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface StockTableRowProps {
|
||||
item: WBStock
|
||||
item: WBStock;
|
||||
}
|
||||
|
||||
export function StockTableRow({ item }: StockTableRowProps) {
|
||||
// Функция для получения изображений карточки
|
||||
const getCardImages = (item: WBStock) => {
|
||||
const fallbackUrl = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp`
|
||||
|
||||
const fallbackUrl = `https://basket-${String(item.nmId).slice(
|
||||
0,
|
||||
2
|
||||
)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(
|
||||
item.nmId
|
||||
).slice(0, -3)}/${item.nmId}/images/big/1.webp`;
|
||||
|
||||
// Проверяем photos
|
||||
if (item.photos && item.photos.length > 0) {
|
||||
return item.photos.map(photo => photo.big || photo.c516x688 || photo.c246x328 || photo.square || photo.tm || fallbackUrl)
|
||||
return item.photos.map(
|
||||
(photo) =>
|
||||
photo.big ||
|
||||
photo.c516x688 ||
|
||||
photo.c246x328 ||
|
||||
photo.square ||
|
||||
photo.tm ||
|
||||
fallbackUrl
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Проверяем mediaFiles
|
||||
if (item.mediaFiles && item.mediaFiles.length > 0) {
|
||||
return item.mediaFiles.map(media => media.big || media.c516x688 || media.c246x328 || media.square || media.tm || fallbackUrl)
|
||||
return item.mediaFiles.map(
|
||||
(media) =>
|
||||
media.big ||
|
||||
media.c516x688 ||
|
||||
media.c246x328 ||
|
||||
media.square ||
|
||||
media.tm ||
|
||||
fallbackUrl
|
||||
);
|
||||
}
|
||||
|
||||
return [fallbackUrl]
|
||||
}
|
||||
|
||||
const getStockStatus = (quantity: number) => {
|
||||
if (quantity === 0) return {
|
||||
color: 'text-red-400',
|
||||
bgColor: 'bg-red-500/10',
|
||||
label: 'Нет в наличии'
|
||||
}
|
||||
if (quantity < 10) return {
|
||||
color: 'text-orange-400',
|
||||
bgColor: 'bg-orange-500/10',
|
||||
label: 'Мало'
|
||||
}
|
||||
return {
|
||||
color: 'text-green-400',
|
||||
bgColor: 'bg-green-500/10',
|
||||
label: 'В наличии'
|
||||
}
|
||||
}
|
||||
|
||||
const stockStatus = getStockStatus(item.totalQuantity)
|
||||
const images = getCardImages(item)
|
||||
const mainImage = images[0] || null
|
||||
return [fallbackUrl];
|
||||
};
|
||||
|
||||
const getStockStatus = (quantity: number) => {
|
||||
if (quantity === 0)
|
||||
return {
|
||||
color: "text-red-400",
|
||||
bgColor: "bg-red-500/10",
|
||||
label: "Нет в наличии",
|
||||
};
|
||||
if (quantity < 10)
|
||||
return {
|
||||
color: "text-orange-400",
|
||||
bgColor: "bg-orange-500/10",
|
||||
label: "Мало",
|
||||
};
|
||||
return {
|
||||
color: "text-green-400",
|
||||
bgColor: "bg-green-500/10",
|
||||
label: "В наличии",
|
||||
};
|
||||
};
|
||||
|
||||
const stockStatus = getStockStatus(item.totalQuantity);
|
||||
const images = getCardImages(item);
|
||||
const mainImage = images[0] || null;
|
||||
|
||||
// Отбираем ключевые характеристики для отображения в таблице
|
||||
const keyCharacteristics = item.characteristics?.slice(0, 3) || []
|
||||
const keyCharacteristics = item.characteristics?.slice(0, 3) || [];
|
||||
|
||||
return (
|
||||
<div className="group">
|
||||
{/* Основная строка товара */}
|
||||
<div
|
||||
className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 transition-all duration-300"
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 transition-all duration-300">
|
||||
{/* Товар (3 колонки) */}
|
||||
<div className="col-span-3 flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden bg-white/10 flex-shrink-0">
|
||||
{mainImage ? (
|
||||
<img
|
||||
<img
|
||||
src={mainImage}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.src = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp`
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = `https://basket-${String(item.nmId).slice(
|
||||
0,
|
||||
2
|
||||
)}.wbbasket.ru/vol${String(item.nmId).slice(
|
||||
0,
|
||||
-5
|
||||
)}/part${String(item.nmId).slice(0, -3)}/${
|
||||
item.nmId
|
||||
}/images/big/1.webp`;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@ -104,16 +133,14 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded">
|
||||
{item.brand || 'Без бренда'}
|
||||
{item.brand || "Без бренда"}
|
||||
</span>
|
||||
<span className="text-white/40 text-xs">#{item.nmId}</span>
|
||||
</div>
|
||||
<h3 className="text-white text-sm font-medium line-clamp-1 mb-1">
|
||||
{item.title}
|
||||
</h3>
|
||||
<div className="text-white/60 text-xs">
|
||||
{item.vendorCode}
|
||||
</div>
|
||||
<div className="text-white/60 text-xs">{item.vendorCode}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -123,7 +150,9 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
<div className={`text-lg font-bold ${stockStatus.color}`}>
|
||||
{item.totalQuantity.toLocaleString()}
|
||||
</div>
|
||||
<div className={`text-xs px-2 py-0.5 rounded ${stockStatus.bgColor} ${stockStatus.color}`}>
|
||||
<div
|
||||
className={`text-xs px-2 py-0.5 rounded ${stockStatus.bgColor} ${stockStatus.color}`}
|
||||
>
|
||||
{stockStatus.label}
|
||||
</div>
|
||||
</div>
|
||||
@ -164,16 +193,22 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
<div className="space-y-1 w-full">
|
||||
{keyCharacteristics.map((char, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span className="text-white/60 truncate w-1/2">{char.name}:</span>
|
||||
<span className="text-white/60 truncate w-1/2">
|
||||
{char.name}:
|
||||
</span>
|
||||
<span className="text-white truncate w-1/2 text-right">
|
||||
{Array.isArray(char.value) ? char.value.join(', ') : String(char.value)}
|
||||
{Array.isArray(char.value)
|
||||
? char.value.join(", ")
|
||||
: String(char.value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{item.subjectName && (
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">Категория:</span>
|
||||
<span className="text-blue-300 truncate text-right">{item.subjectName}</span>
|
||||
<span className="text-blue-300 truncate text-right">
|
||||
{item.subjectName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -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="col-span-12 flex flex-wrap gap-3">
|
||||
{item.stocks.map((stock, stockIndex) => (
|
||||
<div
|
||||
key={`${stock.warehouseId}-${stockIndex}`}
|
||||
<div
|
||||
key={`${stock.warehouseId}-${stockIndex}`}
|
||||
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">
|
||||
{stock.warehouseName}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Цифры */}
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<div className="text-center">
|
||||
<div className={`font-bold ${stock.quantity > 0 ? 'text-green-400' : 'text-white/30'}`}>
|
||||
<div
|
||||
className={`font-bold ${
|
||||
stock.quantity > 0 ? "text-green-400" : "text-white/30"
|
||||
}`}
|
||||
>
|
||||
{stock.quantity}
|
||||
</div>
|
||||
<div className="text-white/50">остаток</div>
|
||||
</div>
|
||||
|
||||
|
||||
{(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && (
|
||||
<>
|
||||
<div className="w-px h-6 bg-white/20" />
|
||||
|
||||
|
||||
{stock.inWayToClient > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-orange-400">{stock.inWayToClient}</div>
|
||||
<div className="font-bold text-orange-400">
|
||||
{stock.inWayToClient}
|
||||
</div>
|
||||
<div className="text-white/50">к клиенту</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{stock.inWayFromClient > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-red-400">{stock.inWayFromClient}</div>
|
||||
<div className="font-bold text-red-400">
|
||||
{stock.inWayFromClient}
|
||||
</div>
|
||||
<div className="text-white/50">от клиента</div>
|
||||
</div>
|
||||
)}
|
||||
@ -227,5 +270,5 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,261 +1,295 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { WildberriesService } from '@/services/wildberries-service'
|
||||
import { toast } from 'sonner'
|
||||
import { StatsCards } from './stats-cards'
|
||||
import { SearchBar } from './search-bar'
|
||||
import { TableHeader } from './table-header'
|
||||
import { LoadingSkeleton } from './loading-skeleton'
|
||||
import { StockTableRow } from './stock-table-row'
|
||||
import { TrendingUp, Package } from 'lucide-react'
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { WildberriesService } from "@/services/wildberries-service";
|
||||
import { toast } from "sonner";
|
||||
import { StatsCards } from "./stats-cards";
|
||||
import { SearchBar } from "./search-bar";
|
||||
import { TableHeader } from "./table-header";
|
||||
import { LoadingSkeleton } from "./loading-skeleton";
|
||||
import { StockTableRow } from "./stock-table-row";
|
||||
import { TrendingUp, Package } from "lucide-react";
|
||||
|
||||
interface WBStock {
|
||||
nmId: number
|
||||
vendorCode: string
|
||||
title: string
|
||||
brand: string
|
||||
price: number
|
||||
nmId: number;
|
||||
vendorCode: string;
|
||||
title: string;
|
||||
brand: string;
|
||||
price: number;
|
||||
stocks: Array<{
|
||||
warehouseId: number
|
||||
warehouseName: string
|
||||
quantity: number
|
||||
quantityFull: number
|
||||
inWayToClient: number
|
||||
inWayFromClient: number
|
||||
}>
|
||||
totalQuantity: number
|
||||
totalReserved: number
|
||||
warehouseId: number;
|
||||
warehouseName: string;
|
||||
quantity: number;
|
||||
quantityFull: number;
|
||||
inWayToClient: number;
|
||||
inWayFromClient: number;
|
||||
}>;
|
||||
totalQuantity: number;
|
||||
totalReserved: number;
|
||||
photos?: Array<{
|
||||
big?: string
|
||||
c246x328?: string
|
||||
c516x688?: string
|
||||
square?: string
|
||||
tm?: string
|
||||
}>
|
||||
mediaFiles?: string[]
|
||||
big?: string;
|
||||
c246x328?: string;
|
||||
c516x688?: string;
|
||||
square?: string;
|
||||
tm?: string;
|
||||
}>;
|
||||
mediaFiles?: string[];
|
||||
characteristics?: Array<{
|
||||
name: string
|
||||
value: string | string[]
|
||||
}>
|
||||
subjectName: string
|
||||
description: string
|
||||
name: string;
|
||||
value: string | string[];
|
||||
}>;
|
||||
subjectName: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface WBWarehouse {
|
||||
id: number
|
||||
name: string
|
||||
cargoType: number
|
||||
deliveryType: number
|
||||
id: number;
|
||||
name: string;
|
||||
cargoType: number;
|
||||
deliveryType: number;
|
||||
}
|
||||
|
||||
export function WBWarehouseDashboard() {
|
||||
const { user } = useAuth()
|
||||
const { isCollapsed, getSidebarMargin } = useSidebar()
|
||||
|
||||
const [stocks, setStocks] = useState<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 { user } = useAuth();
|
||||
const { isCollapsed, getSidebarMargin } = useSidebar();
|
||||
|
||||
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 combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
|
||||
const stocksMap = new Map<number, WBStock>()
|
||||
|
||||
// Создаем карту аналитических данных для быстрого поиска
|
||||
const analyticsMap = new Map() // Map nmId to its analytics data
|
||||
analyticsResults.forEach(result => {
|
||||
analyticsMap.set(result.nmId, result.data)
|
||||
})
|
||||
const combineCardsWithAnalytics = (
|
||||
cards: unknown[],
|
||||
analyticsResults: unknown[]
|
||||
) => {
|
||||
const stocksMap = new Map<number, WBStock>();
|
||||
|
||||
cards.forEach(card => {
|
||||
// Создаем карту аналитических данных для быстрого поиска
|
||||
const analyticsMap = new Map(); // Map nmId to its analytics data
|
||||
analyticsResults.forEach((result) => {
|
||||
analyticsMap.set(result.nmId, result.data);
|
||||
});
|
||||
|
||||
cards.forEach((card) => {
|
||||
const stock: WBStock = {
|
||||
nmId: card.nmID,
|
||||
vendorCode: String(card.vendorCode || card.supplierVendorCode || ''),
|
||||
vendorCode: String(card.vendorCode || card.supplierVendorCode || ""),
|
||||
title: String(card.title || card.object || `Товар ${card.nmID}`),
|
||||
brand: String(card.brand || ''),
|
||||
brand: String(card.brand || ""),
|
||||
price: 0,
|
||||
stocks: [],
|
||||
totalQuantity: 0,
|
||||
totalReserved: 0,
|
||||
photos: Array.isArray(card.photos) ? card.photos : [],
|
||||
mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [],
|
||||
characteristics: Array.isArray(card.characteristics) ? card.characteristics : [],
|
||||
subjectName: String(card.subjectName || card.object || ''),
|
||||
description: String(card.description || '')
|
||||
}
|
||||
|
||||
characteristics: Array.isArray(card.characteristics)
|
||||
? card.characteristics
|
||||
: [],
|
||||
subjectName: String(card.subjectName || card.object || ""),
|
||||
description: String(card.description || ""),
|
||||
};
|
||||
|
||||
if (card.sizes && card.sizes.length > 0) {
|
||||
stock.price = Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0
|
||||
stock.price =
|
||||
Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0;
|
||||
}
|
||||
|
||||
const analyticsData = analyticsMap.get(card.nmID)
|
||||
|
||||
const analyticsData = analyticsMap.get(card.nmID);
|
||||
if (analyticsData?.data?.regions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
analyticsData.data.regions.forEach((region: any) => {
|
||||
(
|
||||
analyticsData.data.regions as {
|
||||
offices: {
|
||||
officeID: number;
|
||||
officeName: string;
|
||||
metrics: { stockCount: number };
|
||||
}[];
|
||||
}[]
|
||||
).forEach((region) => {
|
||||
if (region.offices && region.offices.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
region.offices.forEach((office: any) => {
|
||||
region.offices.forEach((office) => {
|
||||
stock.stocks.push({
|
||||
warehouseId: office.officeID,
|
||||
warehouseName: office.officeName,
|
||||
quantity: office.metrics?.stockCount || 0,
|
||||
quantityFull: office.metrics?.stockCount || 0,
|
||||
inWayToClient: office.metrics?.toClientCount || 0,
|
||||
inWayFromClient: office.metrics?.fromClientCount || 0
|
||||
})
|
||||
|
||||
stock.totalQuantity += office.metrics?.stockCount || 0
|
||||
stock.totalReserved += office.metrics?.toClientCount || 0
|
||||
})
|
||||
inWayFromClient: office.metrics?.fromClientCount || 0,
|
||||
});
|
||||
|
||||
stock.totalQuantity += office.metrics?.stockCount || 0;
|
||||
stock.totalReserved += office.metrics?.toClientCount || 0;
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
stocksMap.set(card.nmID, stock)
|
||||
})
|
||||
|
||||
return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
|
||||
}
|
||||
|
||||
stocksMap.set(card.nmID, stock);
|
||||
});
|
||||
|
||||
return Array.from(stocksMap.values()).sort(
|
||||
(a, b) => b.totalQuantity - a.totalQuantity
|
||||
);
|
||||
};
|
||||
|
||||
// Извлечение информации о складах из данных
|
||||
const extractWarehousesFromStocks = (stocksData: WBStock[]): WBWarehouse[] => {
|
||||
const warehousesMap = new Map<number, WBWarehouse>()
|
||||
|
||||
stocksData.forEach(stock => {
|
||||
stock.stocks.forEach(stockInfo => {
|
||||
const extractWarehousesFromStocks = (
|
||||
stocksData: WBStock[]
|
||||
): WBWarehouse[] => {
|
||||
const warehousesMap = new Map<number, WBWarehouse>();
|
||||
|
||||
stocksData.forEach((stock) => {
|
||||
stock.stocks.forEach((stockInfo) => {
|
||||
if (!warehousesMap.has(stockInfo.warehouseId)) {
|
||||
warehousesMap.set(stockInfo.warehouseId, {
|
||||
id: stockInfo.warehouseId,
|
||||
name: stockInfo.warehouseName,
|
||||
cargoType: 1,
|
||||
deliveryType: 1
|
||||
})
|
||||
deliveryType: 1,
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(warehousesMap.values())
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(warehousesMap.values());
|
||||
};
|
||||
|
||||
// Обновление статистики
|
||||
const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => {
|
||||
setTotalProducts(stocksData.length)
|
||||
setTotalStocks(stocksData.reduce((sum, item) => sum + item.totalQuantity, 0))
|
||||
setTotalReserved(stocksData.reduce((sum, item) => sum + item.totalReserved, 0))
|
||||
|
||||
const totalFromClientCount = stocksData.reduce((sum, item) =>
|
||||
sum + item.stocks.reduce((stockSum, stock) => stockSum + stock.inWayFromClient, 0), 0
|
||||
)
|
||||
setTotalFromClient(totalFromClientCount)
|
||||
|
||||
const updateStatistics = (
|
||||
stocksData: WBStock[],
|
||||
warehousesData: WBWarehouse[]
|
||||
) => {
|
||||
setTotalProducts(stocksData.length);
|
||||
setTotalStocks(
|
||||
stocksData.reduce((sum, item) => sum + item.totalQuantity, 0)
|
||||
);
|
||||
setTotalReserved(
|
||||
stocksData.reduce((sum, item) => sum + item.totalReserved, 0)
|
||||
);
|
||||
|
||||
const totalFromClientCount = stocksData.reduce(
|
||||
(sum, item) =>
|
||||
sum +
|
||||
item.stocks.reduce(
|
||||
(stockSum, stock) => stockSum + stock.inWayFromClient,
|
||||
0
|
||||
),
|
||||
0
|
||||
);
|
||||
setTotalFromClient(totalFromClientCount);
|
||||
|
||||
const warehousesWithStock = new Set(
|
||||
stocksData.flatMap(item => item.stocks.map(s => s.warehouseId))
|
||||
)
|
||||
setActiveWarehouses(warehousesWithStock.size)
|
||||
}
|
||||
stocksData.flatMap((item) => item.stocks.map((s) => s.warehouseId))
|
||||
);
|
||||
setActiveWarehouses(warehousesWithStock.size);
|
||||
};
|
||||
|
||||
// Загрузка данных склада
|
||||
const loadWarehouseData = async () => {
|
||||
if (!user?.wildberriesApiKey) return
|
||||
if (!user?.wildberriesApiKey) return;
|
||||
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
try {
|
||||
const apiToken = user.wildberriesApiKey
|
||||
const wbService = new WildberriesService(apiToken)
|
||||
const apiToken = user.wildberriesApiKey;
|
||||
const wbService = new WildberriesService(apiToken);
|
||||
|
||||
// 1. Получаем карточки товаров
|
||||
const cards = await WildberriesService.getAllCards(apiToken).catch(() => [])
|
||||
console.log('WB Warehouse: Loaded cards:', cards.length)
|
||||
const cards = await WildberriesService.getAllCards(apiToken).catch(
|
||||
() => []
|
||||
);
|
||||
console.log("WB Warehouse: Loaded cards:", cards.length);
|
||||
|
||||
if (cards.length === 0) {
|
||||
toast.error('Нет карточек товаров в WB')
|
||||
return
|
||||
toast.error("Нет карточек товаров в WB");
|
||||
return;
|
||||
}
|
||||
|
||||
const nmIds = cards.map(card => card.nmID).filter(id => id > 0)
|
||||
console.log('WB Warehouse: NM IDs to process:', nmIds.length)
|
||||
const nmIds = cards.map((card) => card.nmID).filter((id) => id > 0);
|
||||
console.log("WB Warehouse: NM IDs to process:", nmIds.length);
|
||||
|
||||
// 2. Получаем аналитику для каждого товара индивидуально
|
||||
const analyticsResults = []
|
||||
const analyticsResults = [];
|
||||
for (const nmId of nmIds) {
|
||||
try {
|
||||
console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`)
|
||||
console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`);
|
||||
const result = await wbService.getStocksReportByOffices({
|
||||
nmIDs: [nmId],
|
||||
stockType: ''
|
||||
})
|
||||
analyticsResults.push({ nmId, data: result })
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
stockType: "",
|
||||
});
|
||||
analyticsResults.push({ nmId, data: result });
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} catch (error) {
|
||||
console.error(`WB Warehouse: Error fetching analytics for nmId ${nmId}:`, error)
|
||||
console.error(
|
||||
`WB Warehouse: Error fetching analytics for nmId ${nmId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('WB Warehouse: Analytics results:', analyticsResults.length)
|
||||
console.log("WB Warehouse: Analytics results:", analyticsResults.length);
|
||||
|
||||
// 3. Комбинируем данные
|
||||
const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults)
|
||||
console.log('WB Warehouse: Combined stocks:', combinedStocks.length)
|
||||
const combinedStocks = combineCardsWithAnalytics(cards, analyticsResults);
|
||||
console.log("WB Warehouse: Combined stocks:", combinedStocks.length);
|
||||
|
||||
// 4. Извлекаем склады и обновляем статистику
|
||||
const extractedWarehouses = extractWarehousesFromStocks(combinedStocks)
|
||||
|
||||
setStocks(combinedStocks)
|
||||
setWarehouses(extractedWarehouses)
|
||||
updateStatistics(combinedStocks, extractedWarehouses)
|
||||
const extractedWarehouses = extractWarehousesFromStocks(combinedStocks);
|
||||
|
||||
toast.success(`Загружено товаров: ${combinedStocks.length}`)
|
||||
} catch (error: any) {
|
||||
console.error('WB Warehouse: Error loading data:', error)
|
||||
toast.error('Ошибка загрузки данных: ' + (error.message || 'Неизвестная ошибка'))
|
||||
setStocks(combinedStocks);
|
||||
setWarehouses(extractedWarehouses);
|
||||
updateStatistics(combinedStocks, extractedWarehouses);
|
||||
|
||||
toast.success(`Загружено товаров: ${combinedStocks.length}`);
|
||||
} catch (error: unknown) {
|
||||
console.error("Error loading warehouse data:", error);
|
||||
toast.error("Ошибка при загрузке данных склада");
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasWBApiKey) {
|
||||
loadWarehouseData()
|
||||
loadWarehouseData();
|
||||
}
|
||||
}, [hasWBApiKey])
|
||||
}, [hasWBApiKey]);
|
||||
|
||||
// Фильтрация товаров
|
||||
const filteredStocks = stocks.filter(item => {
|
||||
if (!searchTerm) return true
|
||||
const search = searchTerm.toLowerCase()
|
||||
const filteredStocks = stocks.filter((item) => {
|
||||
if (!searchTerm) return true;
|
||||
const search = searchTerm.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(search) ||
|
||||
String(item.nmId).includes(search) ||
|
||||
item.brand.toLowerCase().includes(search) ||
|
||||
item.vendorCode.toLowerCase().includes(search)
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
|
||||
<main
|
||||
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
|
||||
>
|
||||
<div className="h-full w-full flex flex-col">
|
||||
|
||||
{/* Результирующие вкладки */}
|
||||
<StatsCards
|
||||
totalProducts={totalProducts}
|
||||
@ -275,16 +309,25 @@ export function WBWarehouseDashboard() {
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{analyticsData.map((warehouse) => (
|
||||
<Card key={warehouse.warehouseId} className="bg-white/5 border-white/10 p-3">
|
||||
<div className="text-sm font-medium text-white mb-2">{warehouse.warehouseName}</div>
|
||||
<Card
|
||||
key={warehouse.warehouseId}
|
||||
className="bg-white/5 border-white/10 p-3"
|
||||
>
|
||||
<div className="text-sm font-medium text-white mb-2">
|
||||
{warehouse.warehouseName}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">К клиенту:</span>
|
||||
<span className="text-green-400 font-medium">{warehouse.toClient}</span>
|
||||
<span className="text-green-400 font-medium">
|
||||
{warehouse.toClient}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">От клиента:</span>
|
||||
<span className="text-orange-400 font-medium">{warehouse.fromClient}</span>
|
||||
<span className="text-orange-400 font-medium">
|
||||
{warehouse.fromClient}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@ -294,10 +337,7 @@ export function WBWarehouseDashboard() {
|
||||
)}
|
||||
|
||||
{/* Поиск */}
|
||||
<SearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
<SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
|
||||
|
||||
{/* Список товаров */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
@ -309,10 +349,15 @@ export function WBWarehouseDashboard() {
|
||||
) : !hasWBApiKey ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-blue-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">Настройте API Wildberries</h3>
|
||||
<p className="text-white/60 mb-4">Для просмотра остатков добавьте API ключ Wildberries в настройках</p>
|
||||
<Button
|
||||
onClick={() => window.location.href = '/settings'}
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Настройте API Wildberries
|
||||
</h3>
|
||||
<p className="text-white/60 mb-4">
|
||||
Для просмотра остатков добавьте API ключ Wildberries в
|
||||
настройках
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => (window.location.href = "/settings")}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Перейти в настройки
|
||||
@ -321,13 +366,17 @@ export function WBWarehouseDashboard() {
|
||||
) : filteredStocks.length === 0 ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">Товары не найдены</h3>
|
||||
<p className="text-white/60">Попробуйте изменить параметры поиска</p>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Товары не найдены
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
Попробуйте изменить параметры поиска
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="overflow-y-auto pr-2 max-h-full">
|
||||
<TableHeader />
|
||||
|
||||
|
||||
{/* Таблица товаров */}
|
||||
<div className="space-y-1">
|
||||
{filteredStocks.map((item, index) => (
|
||||
@ -340,5 +389,5 @@ export function WBWarehouseDashboard() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
import { gql } from "graphql-tag";
|
||||
|
||||
export const SEND_SMS_CODE = gql`
|
||||
mutation SendSmsCode($phone: String!) {
|
||||
@ -7,7 +7,7 @@ export const SEND_SMS_CODE = gql`
|
||||
message
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const VERIFY_SMS_CODE = gql`
|
||||
mutation VerifySmsCode($phone: String!, $code: String!) {
|
||||
@ -56,7 +56,7 @@ export const VERIFY_SMS_CODE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const VERIFY_INN = gql`
|
||||
mutation VerifyInn($inn: String!) {
|
||||
@ -71,10 +71,12 @@ export const VERIFY_INN = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const REGISTER_FULFILLMENT_ORGANIZATION = gql`
|
||||
mutation RegisterFulfillmentOrganization($input: FulfillmentRegistrationInput!) {
|
||||
mutation RegisterFulfillmentOrganization(
|
||||
$input: FulfillmentRegistrationInput!
|
||||
) {
|
||||
registerFulfillmentOrganization(input: $input) {
|
||||
success
|
||||
message
|
||||
@ -119,7 +121,7 @@ export const REGISTER_FULFILLMENT_ORGANIZATION = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const REGISTER_SELLER_ORGANIZATION = gql`
|
||||
mutation RegisterSellerOrganization($input: SellerRegistrationInput!) {
|
||||
@ -167,7 +169,7 @@ export const REGISTER_SELLER_ORGANIZATION = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const ADD_MARKETPLACE_API_KEY = gql`
|
||||
mutation AddMarketplaceApiKey($input: MarketplaceApiKeyInput!) {
|
||||
@ -183,13 +185,13 @@ export const ADD_MARKETPLACE_API_KEY = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const REMOVE_MARKETPLACE_API_KEY = gql`
|
||||
mutation RemoveMarketplaceApiKey($marketplace: MarketplaceType!) {
|
||||
removeMarketplaceApiKey(marketplace: $marketplace)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_USER_PROFILE = gql`
|
||||
mutation UpdateUserProfile($input: UpdateUserProfileInput!) {
|
||||
@ -237,7 +239,7 @@ export const UPDATE_USER_PROFILE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_ORGANIZATION_BY_INN = gql`
|
||||
mutation UpdateOrganizationByInn($inn: String!) {
|
||||
@ -285,12 +287,15 @@ export const UPDATE_ORGANIZATION_BY_INN = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для контрагентов
|
||||
export const SEND_COUNTERPARTY_REQUEST = gql`
|
||||
mutation SendCounterpartyRequest($organizationId: ID!, $message: String) {
|
||||
sendCounterpartyRequest(organizationId: $organizationId, message: $message) {
|
||||
sendCounterpartyRequest(
|
||||
organizationId: $organizationId
|
||||
message: $message
|
||||
) {
|
||||
success
|
||||
message
|
||||
request {
|
||||
@ -315,7 +320,7 @@ export const SEND_COUNTERPARTY_REQUEST = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const RESPOND_TO_COUNTERPARTY_REQUEST = gql`
|
||||
mutation RespondToCounterpartyRequest($requestId: ID!, $accept: Boolean!) {
|
||||
@ -344,24 +349,32 @@ export const RESPOND_TO_COUNTERPARTY_REQUEST = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const CANCEL_COUNTERPARTY_REQUEST = gql`
|
||||
mutation CancelCounterpartyRequest($requestId: ID!) {
|
||||
cancelCounterpartyRequest(requestId: $requestId)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const REMOVE_COUNTERPARTY = gql`
|
||||
mutation RemoveCounterparty($organizationId: ID!) {
|
||||
removeCounterparty(organizationId: $organizationId)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для сообщений
|
||||
export const SEND_MESSAGE = gql`
|
||||
mutation SendMessage($receiverOrganizationId: ID!, $content: String!, $type: MessageType = TEXT) {
|
||||
sendMessage(receiverOrganizationId: $receiverOrganizationId, content: $content, type: $type) {
|
||||
mutation SendMessage(
|
||||
$receiverOrganizationId: ID!
|
||||
$content: String!
|
||||
$type: MessageType = TEXT
|
||||
) {
|
||||
sendMessage(
|
||||
receiverOrganizationId: $receiverOrganizationId
|
||||
content: $content
|
||||
type: $type
|
||||
) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
@ -403,11 +416,19 @@ export const SEND_MESSAGE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const SEND_VOICE_MESSAGE = gql`
|
||||
mutation SendVoiceMessage($receiverOrganizationId: ID!, $voiceUrl: String!, $voiceDuration: Int!) {
|
||||
sendVoiceMessage(receiverOrganizationId: $receiverOrganizationId, voiceUrl: $voiceUrl, voiceDuration: $voiceDuration) {
|
||||
mutation SendVoiceMessage(
|
||||
$receiverOrganizationId: ID!
|
||||
$voiceUrl: String!
|
||||
$voiceDuration: Int!
|
||||
) {
|
||||
sendVoiceMessage(
|
||||
receiverOrganizationId: $receiverOrganizationId
|
||||
voiceUrl: $voiceUrl
|
||||
voiceDuration: $voiceDuration
|
||||
) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
@ -449,11 +470,23 @@ export const SEND_VOICE_MESSAGE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const SEND_IMAGE_MESSAGE = gql`
|
||||
mutation SendImageMessage($receiverOrganizationId: ID!, $fileUrl: String!, $fileName: String!, $fileSize: Int!, $fileType: String!) {
|
||||
sendImageMessage(receiverOrganizationId: $receiverOrganizationId, fileUrl: $fileUrl, fileName: $fileName, fileSize: $fileSize, fileType: $fileType) {
|
||||
mutation SendImageMessage(
|
||||
$receiverOrganizationId: ID!
|
||||
$fileUrl: String!
|
||||
$fileName: String!
|
||||
$fileSize: Int!
|
||||
$fileType: String!
|
||||
) {
|
||||
sendImageMessage(
|
||||
receiverOrganizationId: $receiverOrganizationId
|
||||
fileUrl: $fileUrl
|
||||
fileName: $fileName
|
||||
fileSize: $fileSize
|
||||
fileType: $fileType
|
||||
) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
@ -495,11 +528,23 @@ export const SEND_IMAGE_MESSAGE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const SEND_FILE_MESSAGE = gql`
|
||||
mutation SendFileMessage($receiverOrganizationId: ID!, $fileUrl: String!, $fileName: String!, $fileSize: Int!, $fileType: String!) {
|
||||
sendFileMessage(receiverOrganizationId: $receiverOrganizationId, fileUrl: $fileUrl, fileName: $fileName, fileSize: $fileSize, fileType: $fileType) {
|
||||
mutation SendFileMessage(
|
||||
$receiverOrganizationId: ID!
|
||||
$fileUrl: String!
|
||||
$fileName: String!
|
||||
$fileSize: Int!
|
||||
$fileType: String!
|
||||
) {
|
||||
sendFileMessage(
|
||||
receiverOrganizationId: $receiverOrganizationId
|
||||
fileUrl: $fileUrl
|
||||
fileName: $fileName
|
||||
fileSize: $fileSize
|
||||
fileType: $fileType
|
||||
) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
@ -541,13 +586,13 @@ export const SEND_FILE_MESSAGE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const MARK_MESSAGES_AS_READ = gql`
|
||||
mutation MarkMessagesAsRead($conversationId: ID!) {
|
||||
markMessagesAsRead(conversationId: $conversationId)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для услуг
|
||||
export const CREATE_SERVICE = gql`
|
||||
@ -566,7 +611,7 @@ export const CREATE_SERVICE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_SERVICE = gql`
|
||||
mutation UpdateService($id: ID!, $input: ServiceInput!) {
|
||||
@ -584,13 +629,13 @@ export const UPDATE_SERVICE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const DELETE_SERVICE = gql`
|
||||
mutation DeleteService($id: ID!) {
|
||||
deleteService(id: $id)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для расходников
|
||||
export const CREATE_SUPPLY = gql`
|
||||
@ -617,7 +662,7 @@ export const CREATE_SUPPLY = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_SUPPLY = gql`
|
||||
mutation UpdateSupply($id: ID!, $input: SupplyInput!) {
|
||||
@ -643,13 +688,13 @@ export const UPDATE_SUPPLY = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const DELETE_SUPPLY = gql`
|
||||
mutation DeleteSupply($id: ID!) {
|
||||
deleteSupply(id: $id)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутация для заказа поставки расходников
|
||||
export const CREATE_SUPPLY_ORDER = gql`
|
||||
@ -697,7 +742,7 @@ export const CREATE_SUPPLY_ORDER = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для логистики
|
||||
export const CREATE_LOGISTICS = gql`
|
||||
@ -717,7 +762,7 @@ export const CREATE_LOGISTICS = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_LOGISTICS = gql`
|
||||
mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) {
|
||||
@ -736,13 +781,13 @@ export const UPDATE_LOGISTICS = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const DELETE_LOGISTICS = gql`
|
||||
mutation DeleteLogistics($id: ID!) {
|
||||
deleteLogistics(id: $id)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для товаров оптовика
|
||||
export const CREATE_PRODUCT = gql`
|
||||
@ -775,7 +820,7 @@ export const CREATE_PRODUCT = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_PRODUCT = gql`
|
||||
mutation UpdateProduct($id: ID!, $input: ProductInput!) {
|
||||
@ -807,13 +852,13 @@ export const UPDATE_PRODUCT = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const DELETE_PRODUCT = gql`
|
||||
mutation DeleteProduct($id: ID!) {
|
||||
deleteProduct(id: $id)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для корзины
|
||||
export const ADD_TO_CART = gql`
|
||||
@ -849,7 +894,7 @@ export const ADD_TO_CART = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_CART_ITEM = gql`
|
||||
mutation UpdateCartItem($productId: ID!, $quantity: Int!) {
|
||||
@ -884,7 +929,7 @@ export const UPDATE_CART_ITEM = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const REMOVE_FROM_CART = gql`
|
||||
mutation RemoveFromCart($productId: ID!) {
|
||||
@ -919,13 +964,13 @@ export const REMOVE_FROM_CART = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const CLEAR_CART = gql`
|
||||
mutation ClearCart {
|
||||
clearCart
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для избранного
|
||||
export const ADD_TO_FAVORITES = gql`
|
||||
@ -954,7 +999,7 @@ export const ADD_TO_FAVORITES = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const REMOVE_FROM_FAVORITES = gql`
|
||||
mutation RemoveFromFavorites($productId: ID!) {
|
||||
@ -982,7 +1027,7 @@ export const REMOVE_FROM_FAVORITES = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для категорий
|
||||
export const CREATE_CATEGORY = gql`
|
||||
@ -998,7 +1043,7 @@ export const CREATE_CATEGORY = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_CATEGORY = gql`
|
||||
mutation UpdateCategory($id: ID!, $input: CategoryInput!) {
|
||||
@ -1013,13 +1058,13 @@ export const UPDATE_CATEGORY = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const DELETE_CATEGORY = gql`
|
||||
mutation DeleteCategory($id: ID!) {
|
||||
deleteCategory(id: $id)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутации для сотрудников
|
||||
export const CREATE_EMPLOYEE = gql`
|
||||
@ -1048,7 +1093,7 @@ export const CREATE_EMPLOYEE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_EMPLOYEE = gql`
|
||||
mutation UpdateEmployee($id: ID!, $input: UpdateEmployeeInput!) {
|
||||
@ -1081,19 +1126,19 @@ export const UPDATE_EMPLOYEE = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const DELETE_EMPLOYEE = gql`
|
||||
mutation DeleteEmployee($id: ID!) {
|
||||
deleteEmployee(id: $id)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const UPDATE_EMPLOYEE_SCHEDULE = gql`
|
||||
mutation UpdateEmployeeSchedule($input: UpdateScheduleInput!) {
|
||||
updateEmployeeSchedule(input: $input)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const CREATE_WILDBERRIES_SUPPLY = gql`
|
||||
mutation CreateWildberriesSupply($input: CreateWildberriesSupplyInput!) {
|
||||
@ -1110,7 +1155,7 @@ export const CREATE_WILDBERRIES_SUPPLY = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Админ мутации
|
||||
export const ADMIN_LOGIN = gql`
|
||||
@ -1130,13 +1175,13 @@ export const ADMIN_LOGIN = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const ADMIN_LOGOUT = gql`
|
||||
mutation AdminLogout {
|
||||
adminLogout
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export const CREATE_SUPPLY_SUPPLIER = gql`
|
||||
mutation CreateSupplySupplier($input: CreateSupplySupplierInput!) {
|
||||
@ -1156,4 +1201,46 @@ export const CREATE_SUPPLY_SUPPLIER = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Мутация для обновления статуса заказа поставки
|
||||
export const UPDATE_SUPPLY_ORDER_STATUS = gql`
|
||||
mutation UpdateSupplyOrderStatus($id: ID!, $status: SupplyOrderStatus!) {
|
||||
updateSupplyOrderStatus(id: $id, status: $status) {
|
||||
success
|
||||
message
|
||||
order {
|
||||
id
|
||||
status
|
||||
deliveryDate
|
||||
totalAmount
|
||||
totalItems
|
||||
partner {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
}
|
||||
items {
|
||||
id
|
||||
quantity
|
||||
price
|
||||
totalPrice
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
description
|
||||
price
|
||||
quantity
|
||||
images
|
||||
mainImage
|
||||
category {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -398,14 +398,18 @@ export const resolvers = {
|
||||
|
||||
const suppliers = await prisma.supplySupplier.findMany({
|
||||
where: { organizationId: currentUser.organization.id },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return suppliers;
|
||||
},
|
||||
|
||||
// Логистика конкретной организации
|
||||
organizationLogistics: async (_: unknown, args: { organizationId: string }, context: Context) => {
|
||||
organizationLogistics: async (
|
||||
_: unknown,
|
||||
args: { organizationId: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
@ -3259,38 +3263,38 @@ export const resolvers = {
|
||||
totalItems: totalItems,
|
||||
organizationId: currentUser.organization.id,
|
||||
fulfillmentCenterId: fulfillmentCenterId,
|
||||
status: initialStatus as any,
|
||||
status: initialStatus,
|
||||
items: {
|
||||
create: orderItems,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
partner: {
|
||||
include: {
|
||||
users: true,
|
||||
include: {
|
||||
partner: {
|
||||
include: {
|
||||
users: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
include: {
|
||||
users: true,
|
||||
organization: {
|
||||
include: {
|
||||
users: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
fulfillmentCenter: {
|
||||
include: {
|
||||
users: true,
|
||||
fulfillmentCenter: {
|
||||
include: {
|
||||
users: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Создаем расходники на основе заказанных товаров
|
||||
@ -4643,6 +4647,182 @@ export const resolvers = {
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Обновить статус заказа поставки
|
||||
updateSupplyOrderStatus: async (
|
||||
_: unknown,
|
||||
args: {
|
||||
id: string;
|
||||
status:
|
||||
| "PENDING"
|
||||
| "CONFIRMED"
|
||||
| "IN_TRANSIT"
|
||||
| "DELIVERED"
|
||||
| "CANCELLED";
|
||||
},
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
try {
|
||||
// Находим заказ поставки
|
||||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
OR: [
|
||||
{ organizationId: currentUser.organization.id }, // Создатель заказа
|
||||
{ partnerId: currentUser.organization.id }, // Поставщик
|
||||
{ fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр
|
||||
],
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partner: true,
|
||||
fulfillmentCenter: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
throw new GraphQLError("Заказ поставки не найден или нет доступа");
|
||||
}
|
||||
|
||||
// Обновляем статус заказа
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: { status: args.status },
|
||||
include: {
|
||||
partner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Если статус изменился на DELIVERED, обновляем склад фулфилмента
|
||||
if (args.status === "DELIVERED" && existingOrder.fulfillmentCenterId) {
|
||||
console.log("🚚 Обновляем склад фулфилмента:", {
|
||||
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
|
||||
itemsCount: existingOrder.items.length,
|
||||
items: existingOrder.items.map((item) => ({
|
||||
productName: item.product.name,
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
});
|
||||
|
||||
// Обновляем расходники фулфилмента
|
||||
for (const item of existingOrder.items) {
|
||||
console.log("📦 Обрабатываем товар:", {
|
||||
productName: item.product.name,
|
||||
quantity: item.quantity,
|
||||
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
|
||||
});
|
||||
|
||||
// Ищем существующий расходник
|
||||
const existingSupply = await prisma.supply.findFirst({
|
||||
where: {
|
||||
name: item.product.name,
|
||||
organizationId: existingOrder.fulfillmentCenterId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("🔍 Найден существующий расходник:", !!existingSupply);
|
||||
|
||||
if (existingSupply) {
|
||||
console.log("📈 Обновляем существующий расходник:", {
|
||||
id: existingSupply.id,
|
||||
oldStock: existingSupply.currentStock,
|
||||
newStock: existingSupply.currentStock + item.quantity,
|
||||
});
|
||||
|
||||
// Обновляем количество существующего расходника
|
||||
await prisma.supply.update({
|
||||
where: { id: existingSupply.id },
|
||||
data: {
|
||||
currentStock: existingSupply.currentStock + item.quantity,
|
||||
status: "available", // Меняем статус на "доступен"
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("➕ Создаем новый расходник:", {
|
||||
name: item.product.name,
|
||||
quantity: item.quantity,
|
||||
organizationId: existingOrder.fulfillmentCenterId,
|
||||
});
|
||||
|
||||
// Создаем новый расходник
|
||||
const newSupply = await prisma.supply.create({
|
||||
data: {
|
||||
name: item.product.name,
|
||||
description:
|
||||
item.product.description ||
|
||||
`Поставка от ${existingOrder.partner.name}`,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
unit: "шт",
|
||||
category: item.product.category?.name || "Упаковка",
|
||||
status: "available",
|
||||
date: new Date(),
|
||||
supplier:
|
||||
existingOrder.partner.name ||
|
||||
existingOrder.partner.fullName ||
|
||||
"Не указан",
|
||||
minStock: Math.round(item.quantity * 0.1),
|
||||
currentStock: item.quantity,
|
||||
organizationId: existingOrder.fulfillmentCenterId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ Создан новый расходник:", {
|
||||
id: newSupply.id,
|
||||
name: newSupply.name,
|
||||
currentStock: newSupply.currentStock,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🎉 Склад фулфилмента успешно обновлен!");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Статус заказа поставки обновлен на "${args.status}"`,
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating supply order status:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при обновлении статуса заказа поставки",
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Резолверы типов
|
||||
|
@ -181,6 +181,7 @@ export const typeDefs = gql`
|
||||
|
||||
# Заказы поставок расходников
|
||||
createSupplyOrder(input: SupplyOrderInput!): SupplyOrderResponse!
|
||||
updateSupplyOrderStatus(id: ID!, status: SupplyOrderStatus!): SupplyOrderResponse!
|
||||
|
||||
# Работа с логистикой
|
||||
createLogistics(input: LogisticsInput!): LogisticsResponse!
|
||||
|
Reference in New Issue
Block a user