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