From ac67b1e1ecd3d7e70994fe35dca896c2dfe05e05 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Mon, 28 Jul 2025 13:19:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2=20=D1=81=20?= =?UTF-8?q?"=D0=A3=D0=BF=D0=B0=D0=BA=D0=BE=D0=B2=D0=BA=D0=B0"=20=D0=BD?= =?UTF-8?q?=D0=B0=20"=D0=A0=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8"=20=D0=B2=20=D1=80=D0=B0=D0=B7=D0=BB=D0=B8=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=85=20=D0=B8=20=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=D1=85.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=20=D0=BD=D0=B5=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D1=8F=D1=82=D1=8B=D1=85=20=D0=BF=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BA=D0=B0=D1=85=20=D0=B8=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D0=BE=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8?= =?UTF-8?q?=D0=B5=20GraphQL=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= =?UTF-8?q?=20=D0=B8=20=D1=80=D0=B5=D0=B7=D0=BE=D0=BB=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85.=20=D0=9E=D0=BF=D1=82?= =?UTF-8?q?=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 2 +- src/components/admin/categories-section.tsx | 320 +++++--- .../ui-kit/fulfillment-warehouse-2-demo.tsx | 2 +- .../fulfillment-supplies-dashboard.tsx | 25 +- .../fulfillment-consumables-orders-tab.tsx | 64 +- .../fulfillment-detailed-goods-tab.tsx | 18 +- .../fulfillment-detailed-supplies-tab.tsx | 131 ++- .../fulfillment-goods-tab.tsx | 62 +- .../fulfillment-supplies-tab.tsx | 25 +- .../seller-materials-tab.tsx | 4 +- .../fulfillment-supplies-tab.tsx | 2 +- .../fulfillment-warehouse-dashboard.tsx | 754 +++++++++++++----- .../supplies/direct-supply-creation.tsx | 404 +++++++--- .../fulfillment-supplies-sub-tab.tsx | 6 +- src/graphql/queries.ts | 106 ++- src/graphql/resolvers.ts | 126 ++- src/graphql/typedefs.ts | 3 + 17 files changed, 1542 insertions(+), 512 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 00c9328..1b458c9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -200,7 +200,7 @@ model Supply { price Decimal @db.Decimal(10, 2) quantity Int @default(0) unit String @default("шт") - category String @default("Упаковка") + category String @default("Расходники") status String @default("planned") // planned, in-transit, delivered, in-stock date DateTime @default(now()) supplier String @default("Не указан") diff --git a/src/components/admin/categories-section.tsx b/src/components/admin/categories-section.tsx index a7bf216..1b287d4 100644 --- a/src/components/admin/categories-section.tsx +++ b/src/components/admin/categories-section.tsx @@ -1,162 +1,190 @@ -"use client" +"use client"; -import { useState } from 'react' -import { useQuery, useMutation } from '@apollo/client' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog' -import { GET_CATEGORIES } from '@/graphql/queries' -import { CREATE_CATEGORY, UPDATE_CATEGORY, DELETE_CATEGORY } from '@/graphql/mutations' -import { Plus, Edit, Trash2, Package } from 'lucide-react' -import { toast } from 'sonner' +import { useState } from "react"; +import { useQuery, useMutation } from "@apollo/client"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { GET_CATEGORIES } from "@/graphql/queries"; +import { + CREATE_CATEGORY, + UPDATE_CATEGORY, + DELETE_CATEGORY, +} from "@/graphql/mutations"; +import { Plus, Edit, Trash2, Package } from "lucide-react"; +import { toast } from "sonner"; interface Category { - id: string - name: string - createdAt: string - updatedAt: string + id: string; + name: string; + createdAt: string; + updatedAt: string; } export function CategoriesSection() { - const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) - const [editingCategory, setEditingCategory] = useState(null) - const [newCategoryName, setNewCategoryName] = useState('') - const [editCategoryName, setEditCategoryName] = useState('') + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); + const [newCategoryName, setNewCategoryName] = useState(""); + const [editCategoryName, setEditCategoryName] = useState(""); const formatDate = (dateString: string) => { try { - const date = new Date(dateString) + const date = new Date(dateString); if (isNaN(date.getTime())) { - return 'Неизвестно' + return "Неизвестно"; } - return date.toLocaleDateString('ru-RU', { - day: '2-digit', - month: '2-digit', - year: 'numeric' - }) + return date.toLocaleDateString("ru-RU", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); } catch (error) { - return 'Неизвестно' + return "Неизвестно"; } - } + }; - const { data, loading, error, refetch } = useQuery(GET_CATEGORIES) - const [createCategory, { loading: creating }] = useMutation(CREATE_CATEGORY) - const [updateCategory, { loading: updating }] = useMutation(UPDATE_CATEGORY) - const [deleteCategory, { loading: deleting }] = useMutation(DELETE_CATEGORY) + const { data, loading, error, refetch } = useQuery(GET_CATEGORIES); + const [createCategory, { loading: creating }] = useMutation(CREATE_CATEGORY); + const [updateCategory, { loading: updating }] = useMutation(UPDATE_CATEGORY); + const [deleteCategory, { loading: deleting }] = useMutation(DELETE_CATEGORY); - const categories: Category[] = data?.categories || [] + const categories: Category[] = data?.categories || []; const handleCreateCategory = async () => { if (!newCategoryName.trim()) { - toast.error('Введите название категории') - return + toast.error("Введите название категории"); + return; } try { const { data } = await createCategory({ - variables: { input: { name: newCategoryName.trim() } } - }) + variables: { input: { name: newCategoryName.trim() } }, + }); if (data?.createCategory?.success) { - toast.success('Категория успешно создана') - setNewCategoryName('') - setIsCreateDialogOpen(false) - refetch() + toast.success("Категория успешно создана"); + setNewCategoryName(""); + setIsCreateDialogOpen(false); + refetch(); } else { - toast.error(data?.createCategory?.message || 'Ошибка при создании категории') + toast.error( + data?.createCategory?.message || "Ошибка при создании категории" + ); } } catch (error) { - console.error('Error creating category:', error) - toast.error('Ошибка при создании категории') + console.error("Error creating category:", error); + toast.error("Ошибка при создании категории"); } - } + }; const handleEditCategory = (category: Category) => { - setEditingCategory(category) - setEditCategoryName(category.name) - setIsEditDialogOpen(true) - } + setEditingCategory(category); + setEditCategoryName(category.name); + setIsEditDialogOpen(true); + }; const handleUpdateCategory = async () => { if (!editingCategory || !editCategoryName.trim()) { - toast.error('Введите название категории') - return + toast.error("Введите название категории"); + return; } try { const { data } = await updateCategory({ - variables: { - id: editingCategory.id, - input: { name: editCategoryName.trim() } - } - }) + variables: { + id: editingCategory.id, + input: { name: editCategoryName.trim() }, + }, + }); if (data?.updateCategory?.success) { - toast.success('Категория успешно обновлена') - setEditingCategory(null) - setEditCategoryName('') - setIsEditDialogOpen(false) - refetch() + toast.success("Категория успешно обновлена"); + setEditingCategory(null); + setEditCategoryName(""); + setIsEditDialogOpen(false); + refetch(); } else { - toast.error(data?.updateCategory?.message || 'Ошибка при обновлении категории') + toast.error( + data?.updateCategory?.message || "Ошибка при обновлении категории" + ); } } catch (error) { - console.error('Error updating category:', error) - toast.error('Ошибка при обновлении категории') + console.error("Error updating category:", error); + toast.error("Ошибка при обновлении категории"); } - } + }; const handleDeleteCategory = async (categoryId: string) => { try { const { data } = await deleteCategory({ - variables: { id: categoryId } - }) + variables: { id: categoryId }, + }); if (data?.deleteCategory) { - toast.success('Категория успешно удалена') - refetch() + toast.success("Категория успешно удалена"); + refetch(); } else { - toast.error('Ошибка при удалении категории') + toast.error("Ошибка при удалении категории"); } } catch (error) { - console.error('Error deleting category:', error) - const errorMessage = error instanceof Error ? error.message : 'Ошибка при удалении категории' - toast.error(errorMessage) + console.error("Error deleting category:", error); + const errorMessage = + error instanceof Error + ? error.message + : "Ошибка при удалении категории"; + toast.error(errorMessage); } - } + }; const handleCreateBasicCategories = async () => { const basicCategories = [ - 'Электроника', - 'Одежда', - 'Обувь', - 'Дом и сад', - 'Красота и здоровье', - 'Спорт и отдых', - 'Автотовары', - 'Детские товары', - 'Продукты питания', - 'Книги и канцелярия' - ] + "Электроника", + "Одежда", + "Обувь", + "Дом и сад", + "Красота и здоровье", + "Спорт и отдых", + "Автотовары", + "Детские товары", + "Продукты питания", + "Книги и канцелярия", + "Расходники", + ]; try { for (const categoryName of basicCategories) { await createCategory({ - variables: { input: { name: categoryName } } - }) + variables: { input: { name: categoryName } }, + }); } - - toast.success('Базовые категории созданы') - refetch() + + toast.success("Базовые категории созданы"); + refetch(); } catch (error) { - console.error('Error creating basic categories:', error) - toast.error('Ошибка при создании категорий') + console.error("Error creating basic categories:", error); + toast.error("Ошибка при создании категорий"); } - } + }; if (loading) { return ( @@ -170,7 +198,7 @@ export function CategoriesSection() { - ) + ); } if (error) { @@ -182,13 +210,17 @@ export function CategoriesSection() {

Ошибка загрузки категорий

-
- ) + ); } return ( @@ -196,12 +228,14 @@ export function CategoriesSection() {

Категории товаров

-

Управление категориями для классификации товаров

+

+ Управление категориями для классификации товаров +

- +
{categories.length === 0 && ( - -
@@ -257,17 +300,21 @@ export function CategoriesSection() { - Список категорий ({categories.length}) + + Список категорий ({categories.length}) + {categories.length === 0 ? (
-

Нет категорий

+

+ Нет категорий +

Создайте категории для классификации товаров

- -
- ) -} \ No newline at end of file + ); +} diff --git a/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx b/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx index 8b36190..9d06a7c 100644 --- a/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx +++ b/src/components/admin/ui-kit/fulfillment-warehouse-2-demo.tsx @@ -362,7 +362,7 @@ export function FulfillmentWarehouse2Demo() { icon={Wrench} current={warehouseStats.fulfillmentSupplies.current} change={warehouseStats.fulfillmentSupplies.change} - description="Упаковка, этикетки" + description="Расходники, этикетки" /> + {count > 99 ? "99+" : count} + + ); +} + export function FulfillmentSuppliesDashboard() { const { getSidebarMargin } = useSidebar(); const [activeTab, setActiveTab] = useState("fulfillment"); + // Загружаем данные о непринятых поставках + const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + }); + + const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; + return (
@@ -32,13 +54,14 @@ export function FulfillmentSuppliesDashboard() { Поставки на фулфилмент Фулфилмент + +
+
+ +
+
+

+ + Требует вашего внимания +

+
+ {supplyOrdersCount > 0 && ( +

+ • {supplyOrdersCount} поставок требуют вашего действия + (подтверждение/получение) +

+ )} + {incomingRequestsCount > 0 && ( +

+ • {incomingRequestsCount} заявок на партнерство ожидают ответа +

+ )} +
+
+
+
+ {pendingCount > 99 ? "99+" : pendingCount} +
+
+
+ + ); +} + interface SupplyOrder { id: string; partnerId: string; @@ -90,6 +148,7 @@ export function FulfillmentConsumablesOrdersTab() { refetchQueries: [ { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок { query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента + { query: GET_WAREHOUSE_PRODUCTS }, // Обновляем товары склада ], onError: (error) => { console.error("Error updating supply order status:", error); @@ -227,6 +286,9 @@ export function FulfillmentConsumablesOrdersTab() { return (
+ {/* Уведомления о непринятых поставках */} + + {/* Компактная статистика */}
diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-goods-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-goods-tab.tsx index 197e4f5..281ecce 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-goods-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-goods-tab.tsx @@ -126,7 +126,7 @@ const mockFulfillmentGoodsSupplies: FulfillmentSupply[] = [ value: "12", unit: "мес", }, - { id: "ffparam4", name: "Упаковка ФФ", value: "Усиленная" }, + { id: "ffparam4", name: "Расходники ФФ", value: "Усиленная" }, ], }, ], @@ -514,7 +514,7 @@ export function FulfillmentDetailedGoodsTab() { const isRouteExpanded = expandedRoutes.has(route.id); return ( - toggleRouteExpansion(route.id)} > @@ -614,9 +614,11 @@ export function FulfillmentDetailedGoodsTab() { ); return ( - toggleSellerExpansion(seller.id)} + onClick={() => + toggleSellerExpansion(seller.id) + } >
@@ -693,9 +695,13 @@ export function FulfillmentDetailedGoodsTab() { expandedProducts.has(product.id); return ( - toggleProductExpansion(product.id)} + onClick={() => + toggleProductExpansion( + product.id + ) + } >
diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx index a24293f..3701f5b 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx @@ -8,7 +8,10 @@ import { StatsCard } from "../../supplies/ui/stats-card"; import { StatsGrid } from "../../supplies/ui/stats-grid"; import { useRouter } from "next/navigation"; import { useQuery } from "@apollo/client"; -import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; +import { + GET_SUPPLY_ORDERS, + GET_PENDING_SUPPLIES_COUNT, +} from "@/graphql/queries"; import { useAuth } from "@/hooks/useAuth"; import { Calendar, @@ -20,8 +23,54 @@ import { Plus, ChevronDown, ChevronRight, + Bell, + AlertTriangle, } from "lucide-react"; +// Компонент уведомлений о непринятых поставках +function PendingSuppliesAlert() { + const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + }); + + const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; + const supplyOrdersCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0; + const incomingRequestsCount = pendingData?.pendingSuppliesCount?.incomingRequests || 0; + + if (pendingCount === 0) return null; + + return ( + +
+
+ +
+
+

+ + Требует вашего внимания +

+
+ {supplyOrdersCount > 0 && ( +

• {supplyOrdersCount} поставок требуют вашего действия (подтверждение/получение)

+ )} + {incomingRequestsCount > 0 && ( +

• {incomingRequestsCount} заявок на партнерство ожидают ответа

+ )} +
+
+
+
+ {pendingCount > 99 ? "99+" : pendingCount} +
+
+
+
+ ); +} + // Интерфейс для заказа interface SupplyOrder { id: string; @@ -63,21 +112,36 @@ const formatDate = (dateString: string) => { // Функция для отображения статуса const getStatusBadge = (status: string) => { const statusConfig = { - PENDING: { label: "Ожидает", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30" }, - CONFIRMED: { label: "Подтверждён", color: "bg-blue-500/20 text-blue-300 border-blue-500/30" }, - IN_PROGRESS: { label: "В работе", color: "bg-purple-500/20 text-purple-300 border-purple-500/30" }, - SHIPPED: { label: "Отправлен", color: "bg-orange-500/20 text-orange-300 border-orange-500/30" }, - DELIVERED: { label: "Доставлен", color: "bg-green-500/20 text-green-300 border-green-500/30" }, - CANCELLED: { label: "Отменён", color: "bg-red-500/20 text-red-300 border-red-500/30" }, + PENDING: { + label: "Ожидает", + color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + }, + CONFIRMED: { + label: "Подтверждён", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + }, + IN_PROGRESS: { + label: "В работе", + color: "bg-purple-500/20 text-purple-300 border-purple-500/30", + }, + SHIPPED: { + label: "Отправлен", + color: "bg-orange-500/20 text-orange-300 border-orange-500/30", + }, + DELIVERED: { + label: "Доставлен", + color: "bg-green-500/20 text-green-300 border-green-500/30", + }, + CANCELLED: { + label: "Отменён", + color: "bg-red-500/20 text-red-300 border-red-500/30", + }, }; - const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.PENDING; + const config = + statusConfig[status as keyof typeof statusConfig] || statusConfig.PENDING; - return ( - - {config.label} - - ); + return {config.label}; }; export function FulfillmentDetailedSuppliesTab() { @@ -87,13 +151,13 @@ export function FulfillmentDetailedSuppliesTab() { // Загружаем реальные данные заказов расходников const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, { - fetchPolicy: 'cache-and-network', // Принудительно проверяем сервер - notifyOnNetworkStatusChange: true + fetchPolicy: "cache-and-network", // Принудительно проверяем сервер + notifyOnNetworkStatusChange: true, }); // Получаем ID текущей организации (фулфилмент-центра) const currentOrganizationId = user?.organization?.id; - + // Фильтруем заказы созданные текущей организацией (наши расходники) const ourSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( (order: SupplyOrder) => order.organizationId === currentOrganizationId @@ -113,7 +177,9 @@ export function FulfillmentDetailedSuppliesTab() { return (
- Загрузка наших расходников... + + Загрузка наших расходников... +
); } @@ -123,7 +189,9 @@ export function FulfillmentDetailedSuppliesTab() {
-

Ошибка загрузки расходников

+

+ Ошибка загрузки расходников +

{error.message}

@@ -132,18 +200,21 @@ export function FulfillmentDetailedSuppliesTab() { return (
+ {/* Уведомления о непринятых поставках */} + + {/* Заголовок с кнопкой создания поставки */}
-

- Наши расходники -

+

Наши расходники

Управление поставками расходников фулфилмента

@@ -225,8 +300,12 @@ export function FulfillmentDetailedSuppliesTab() { Дата создания - План - Факт + + План + + + Факт + Цена расходников diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx index 89ef2d5..d295c8d 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx @@ -13,7 +13,11 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { GET_MY_EMPLOYEES, GET_MY_COUNTERPARTIES } from "@/graphql/queries"; +import { + GET_MY_EMPLOYEES, + GET_MY_COUNTERPARTIES, + GET_PENDING_SUPPLIES_COUNT, +} from "@/graphql/queries"; import { Package, Plus, @@ -31,9 +35,62 @@ import { Clock, CheckCircle, FileText, + Bell, + AlertTriangle, } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +// Компонент уведомлений о непринятых поставках +function PendingSuppliesAlert() { + const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + }); + + const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; + const supplyOrdersCount = + pendingData?.pendingSuppliesCount?.supplyOrders || 0; + const incomingRequestsCount = + pendingData?.pendingSuppliesCount?.incomingRequests || 0; + + if (pendingCount === 0) return null; + + return ( + +
+
+ +
+
+

+ + Требует вашего внимания +

+
+ {supplyOrdersCount > 0 && ( +

+ • {supplyOrdersCount} поставок требуют вашего действия + (подтверждение/получение) +

+ )} + {incomingRequestsCount > 0 && ( +

+ • {incomingRequestsCount} заявок на партнерство ожидают ответа +

+ )} +
+
+
+
+ {pendingCount > 99 ? "99+" : pendingCount} +
+
+
+
+ ); +} + // Интерфейсы для данных interface Employee { id: string; @@ -655,6 +712,9 @@ export function FulfillmentGoodsTab() { return (
+ {/* Уведомления о непринятых поставках */} + + + {count > 99 ? "99+" : count} +
+ ); +} + export function FulfillmentSuppliesTab() { const router = useRouter(); const searchParams = useSearchParams(); const [activeTab, setActiveTab] = useState("goods"); + // Загружаем данные о непринятых поставках + const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + }); + + const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; + // Проверяем URL параметр при загрузке useEffect(() => { const tabParam = searchParams.get("tab"); @@ -66,12 +88,13 @@ export function FulfillmentSuppliesTab() { Расходники селлеров Селлеры С + partner.type === "SELLER" + (partner: { type: string }) => partner.type === "SELLER" ); const supplyOrders: SupplyOrder[] = ordersData?.supplyOrders || []; + const allProducts = productsData?.warehouseProducts || []; + const mySupplies = suppliesData?.mySupplies || []; // Добавляем расходники // Логирование для отладки console.log("🏪 Данные склада фулфилмента:", { @@ -201,13 +226,147 @@ export function FulfillmentWarehouseDashboard() { ordersCount: supplyOrders.length, deliveredOrders: supplyOrders.filter((o) => o.status === "DELIVERED") .length, + productsCount: allProducts.length, + suppliesCount: mySupplies.length, // Добавляем логирование расходников + supplies: mySupplies.map((s: any) => ({ + id: s.id, + name: s.name, + currentStock: s.currentStock, + category: s.category, + supplier: s.supplier, + })), + products: allProducts.map((p: any) => ({ + id: p.id, + name: p.name, + article: p.article, + organizationName: p.organization?.name || p.organization?.fullName, + organizationType: p.organization?.type, + })), + // Добавляем анализ соответствия товаров и расходников + productSupplyMatching: allProducts.map((product: any) => { + const matchingSupply = mySupplies.find((supply: any) => { + return ( + supply.name.toLowerCase() === product.name.toLowerCase() || + supply.name + .toLowerCase() + .includes(product.name.toLowerCase().split(" ")[0]) + ); + }); + return { + productName: product.name, + matchingSupplyName: matchingSupply?.name, + matchingSupplyStock: matchingSupply?.currentStock, + hasMatch: !!matchingSupply, + }; + }), counterpartiesLoading, ordersLoading, + productsLoading, + suppliesLoading, // Добавляем статус загрузки расходников counterpartiesError: counterpartiesError?.message, ordersError: ordersError?.message, + productsError: productsError?.message, + suppliesError: suppliesError?.message, // Добавляем ошибки загрузки расходников }); - // Подсчитываем статистику на основе реальных данных партнеров-селлеров + // Расчет поступлений расходников за сутки (выносим отдельно для использования в storeData) + const suppliesReceivedToday = useMemo(() => { + const deliveredOrders = supplyOrders.filter( + (o) => o.status === "DELIVERED" + ); + + // Подсчитываем расходники селлера из доставленных заказов за последние сутки + const oneDayAgo = new Date(); + oneDayAgo.setDate(oneDayAgo.getDate() - 1); + + const recentDeliveredOrders = deliveredOrders.filter((order) => { + const deliveryDate = new Date(order.deliveryDate); + return deliveryDate >= oneDayAgo && order.fulfillmentCenter?.id; // За последние сутки + }); + + const realSuppliesReceived = recentDeliveredOrders.reduce( + (sum, order) => sum + order.totalItems, + 0 + ); + + // Логирование для отладки + console.log("📦 Анализ поставок расходников за сутки:", { + totalDeliveredOrders: deliveredOrders.length, + recentDeliveredOrders: recentDeliveredOrders.length, + recentOrders: recentDeliveredOrders.map((order) => ({ + id: order.id, + deliveryDate: order.deliveryDate, + totalItems: order.totalItems, + status: order.status, + })), + realSuppliesReceived, + oneDayAgo: oneDayAgo.toISOString(), + }); + + // Возвращаем реальное значение без fallback + return realSuppliesReceived; + }, [supplyOrders]); + + // Расчет использованных расходников за сутки (пока всегда 0, так как нет данных об использовании) + const suppliesUsedToday = useMemo(() => { + // TODO: Здесь должна быть логика подсчета использованных расходников + // Пока возвращаем 0, так как нет данных об использовании + return 0; + }, []); + + // Расчет изменений товаров за сутки (реальные данные) + const productsReceivedToday = useMemo(() => { + // Товары, поступившие за сутки из доставленных заказов + const deliveredOrders = supplyOrders.filter( + (o) => o.status === "DELIVERED" + ); + const oneDayAgo = new Date(); + oneDayAgo.setDate(oneDayAgo.getDate() - 1); + + const recentDeliveredOrders = deliveredOrders.filter((order) => { + const deliveryDate = new Date(order.deliveryDate); + return deliveryDate >= oneDayAgo && order.fulfillmentCenter?.id; + }); + + const realProductsReceived = recentDeliveredOrders.reduce( + (sum, order) => sum + (order.totalItems || 0), + 0 + ); + + // Логирование для отладки + console.log("📦 Анализ поставок товаров за сутки:", { + totalDeliveredOrders: deliveredOrders.length, + recentDeliveredOrders: recentDeliveredOrders.length, + recentOrders: recentDeliveredOrders.map((order) => ({ + id: order.id, + deliveryDate: order.deliveryDate, + totalItems: order.totalItems, + status: order.status, + })), + realProductsReceived, + oneDayAgo: oneDayAgo.toISOString(), + }); + + return realProductsReceived; + }, [supplyOrders]); + + const productsUsedToday = useMemo(() => { + // Товары, отправленные/использованные за сутки (пока 0, нет данных) + return 0; + }, []); + + // Логирование статистики расходников для отладки + console.log("📊 Статистика расходников селлера:", { + suppliesReceivedToday, + suppliesUsedToday, + totalSellerSupplies: mySupplies.reduce( + (sum: number, supply: any) => sum + (supply.currentStock || 0), + 0 + ), + netChange: suppliesReceivedToday - suppliesUsedToday, + }); + + // Подсчитываем статистику на основе реальных данных из заказов поставок const warehouseStats: WarehouseStats = useMemo(() => { const inTransitOrders = supplyOrders.filter( (o) => o.status === "IN_TRANSIT" @@ -216,134 +375,381 @@ export function FulfillmentWarehouseDashboard() { (o) => o.status === "DELIVERED" ); - // Генерируем статистику на основе количества партнеров-селлеров - const baseMultiplier = sellerPartners.length * 100; + // Подсчитываем общее количество товаров из всех доставленных заказов + const totalProductsFromOrders = allProducts.reduce( + (sum, product: any) => sum + (product.orderedQuantity || 0), + 0 + ); + + // Подсчитываем реальное количество расходников селлера из таблицы supplies + const totalSellerSupplies = mySupplies.reduce( + (sum: number, supply: any) => sum + (supply.currentStock || 0), + 0 + ); return { products: { - current: baseMultiplier + 450, - change: 105, + current: totalProductsFromOrders, // Реальное количество товаров на складе + change: productsReceivedToday - productsUsedToday, // Реальное изменение за сутки }, goods: { - current: Math.floor(baseMultiplier * 0.6) + 200, - change: 77, + current: 0, // Нет реальных данных о готовых товарах + change: 0, // Нет реальных данных об изменениях готовых товаров }, defects: { - current: Math.floor(baseMultiplier * 0.05) + 15, - change: -15, + current: 0, // Нет реальных данных о браке + change: 0, // Нет реальных данных об изменениях брака }, pvzReturns: { - current: Math.floor(baseMultiplier * 0.1) + 50, - change: 36, + current: 0, // Нет реальных данных о возвратах с ПВЗ + change: 0, // Нет реальных данных об изменениях возвратов }, fulfillmentSupplies: { - current: Math.floor(baseMultiplier * 0.3) + 80, - change: deliveredOrders.length, + current: 0, // Нет реальных данных о расходниках ФФ + change: 0, // Нет реальных данных об изменениях расходников ФФ }, sellerSupplies: { - current: inTransitOrders.reduce((sum, o) => sum + o.totalItems, 0) + Math.floor(baseMultiplier * 0.2), - change: 57, + current: totalSellerSupplies, // Реальное количество расходников селлера из базы + change: suppliesReceivedToday - suppliesUsedToday, // Реальное изменение за сутки }, }; - }, [sellerPartners, supplyOrders]); + }, [ + sellerPartners, + supplyOrders, + allProducts, + mySupplies, + suppliesReceivedToday, + suppliesUsedToday, + productsReceivedToday, + productsUsedToday, + ]); - // Создаем структурированные данные склада на основе партнеров-селлеров + // Создаем структурированные данные склада на основе уникальных товаров const storeData: StoreData[] = useMemo(() => { - if (!sellerPartners.length) return []; + if (!sellerPartners.length && !allProducts.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 groupedProducts = new Map< + string, + { + name: string; + totalQuantity: number; + suppliers: string[]; + categories: string[]; + prices: number[]; + articles: string[]; + originalProducts: any[]; + } + >(); - // Создаем товары для партнера - 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, - }; + // Группируем товары из allProducts + allProducts.forEach((product: any) => { + const productName = product.name; + const quantity = product.orderedQuantity || 0; + + if (groupedProducts.has(productName)) { + const existing = groupedProducts.get(productName)!; + existing.totalQuantity += quantity; + existing.suppliers.push( + product.organization?.name || + product.organization?.fullName || + "Неизвестно" + ); + existing.categories.push(product.category?.name || "Без категории"); + existing.prices.push(product.price || 0); + existing.articles.push(product.article || ""); + existing.originalProducts.push(product); + } else { + groupedProducts.set(productName, { + name: productName, + totalQuantity: quantity, + suppliers: [ + product.organization?.name || + product.organization?.fullName || + "Неизвестно", + ], + categories: [product.category?.name || "Без категории"], + prices: [product.price || 0], + articles: [product.article || ""], + originalProducts: [product], + }); + } + }); + + // Группируем расходники по названию + const groupedSupplies = new Map(); + mySupplies.forEach((supply: any) => { + const supplyName = supply.name; + const currentStock = supply.currentStock || 0; + + if (groupedSupplies.has(supplyName)) { + groupedSupplies.set( + supplyName, + groupedSupplies.get(supplyName)! + currentStock + ); + } else { + groupedSupplies.set(supplyName, currentStock); + } + }); + + // Логирование группировки + console.log("📊 Группировка товаров и расходников:", { + groupedProductsCount: groupedProducts.size, + groupedSuppliesCount: groupedSupplies.size, + groupedProducts: Array.from(groupedProducts.entries()).map( + ([name, data]) => ({ + name, + totalQuantity: data.totalQuantity, + suppliersCount: data.suppliers.length, + uniqueSuppliers: [...new Set(data.suppliers)], + }) + ), + groupedSupplies: Array.from(groupedSupplies.entries()).map( + ([name, quantity]) => ({ + name, + totalQuantity: quantity, + }) + ), + }); + + // Создаем виртуальных "партнеров" на основе уникальных товаров + const uniqueProductNames = Array.from(groupedProducts.keys()); + const virtualPartners = Math.max( + 1, + Math.min(sellerPartners.length, Math.ceil(uniqueProductNames.length / 8)) + ); + + return Array.from({ length: virtualPartners }, (_, index) => { + const startIndex = index * 8; + const endIndex = Math.min(startIndex + 8, uniqueProductNames.length); + const partnerProductNames = uniqueProductNames.slice( + startIndex, + endIndex + ); + + const items: ProductItem[] = partnerProductNames.map( + (productName, itemIndex) => { + const productData = groupedProducts.get(productName)!; + const itemProducts = productData.totalQuantity; + + // Ищем соответствующий расходник по названию + const matchingSupplyQuantity = groupedSupplies.get(productName) || 0; + + // Если нет точного совпадения, ищем частичное совпадение + let itemSuppliesQuantity = matchingSupplyQuantity; + if (itemSuppliesQuantity === 0) { + for (const [supplyName, quantity] of groupedSupplies.entries()) { + if ( + supplyName.toLowerCase().includes(productName.toLowerCase()) || + productName.toLowerCase().includes(supplyName.toLowerCase()) + ) { + itemSuppliesQuantity = quantity; + break; + } + } + } + + // Fallback к процентному соотношению + if (itemSuppliesQuantity === 0) { + itemSuppliesQuantity = Math.floor(itemProducts * 0.1); + } + + console.log(`📦 Товар "${productName}":`, { + totalQuantity: itemProducts, + suppliersCount: productData.suppliers.length, + uniqueSuppliers: [...new Set(productData.suppliers)], + matchingSupplyQuantity: matchingSupplyQuantity, + finalSuppliesQuantity: itemSuppliesQuantity, + usedFallback: + matchingSupplyQuantity === 0 && itemSuppliesQuantity > 0, + }); + + return { + id: `grouped-${productName}-${itemIndex}`, // Уникальный ID для группированного товара + name: productName, + article: + productData.articles[0] || + `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: 0, // Нет реальных данных о готовых товарах + defectsPlace: `C${index + 1}-${itemIndex + 1}`, + defectsQuantity: 0, // Нет реальных данных о браке + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}`, + sellerSuppliesQuantity: itemSuppliesQuantity, // Суммированное количество расходников (реальные данные) + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}`, + pvzReturnsQuantity: 0, // Нет реальных данных о возвратах с ПВЗ + // Создаем варианты товара + variants: + Math.random() > 0.5 + ? [ + { + id: `grouped-${productName}-${itemIndex}-1`, + name: `Размер S`, + productPlace: `A${index + 1}-${itemIndex + 1}-1`, + productQuantity: Math.floor(itemProducts * 0.4), // Часть от общего количества + goodsPlace: `B${index + 1}-${itemIndex + 1}-1`, + goodsQuantity: 0, // Нет реальных данных о готовых товарах + defectsPlace: `C${index + 1}-${itemIndex + 1}-1`, + defectsQuantity: 0, // Нет реальных данных о браке + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-1`, + sellerSuppliesQuantity: Math.floor( + itemSuppliesQuantity * 0.4 + ), // Часть от расходников + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-1`, + pvzReturnsQuantity: 0, // Нет реальных данных о возвратах + }, + { + id: `grouped-${productName}-${itemIndex}-2`, + name: `Размер M`, + productPlace: `A${index + 1}-${itemIndex + 1}-2`, + productQuantity: Math.floor(itemProducts * 0.4), // Часть от общего количества + goodsPlace: `B${index + 1}-${itemIndex + 1}-2`, + goodsQuantity: 0, // Нет реальных данных о готовых товарах + defectsPlace: `C${index + 1}-${itemIndex + 1}-2`, + defectsQuantity: 0, // Нет реальных данных о браке + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-2`, + sellerSuppliesQuantity: Math.floor( + itemSuppliesQuantity * 0.4 + ), // Часть от расходников + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-2`, + pvzReturnsQuantity: 0, // Нет реальных данных о возвратах + }, + { + id: `grouped-${productName}-${itemIndex}-3`, + name: `Размер L`, + productPlace: `A${index + 1}-${itemIndex + 1}-3`, + productQuantity: Math.floor(itemProducts * 0.2), // Оставшаяся часть + goodsPlace: `B${index + 1}-${itemIndex + 1}-3`, + goodsQuantity: 0, // Нет реальных данных о готовых товарах + defectsPlace: `C${index + 1}-${itemIndex + 1}-3`, + defectsQuantity: 0, // Нет реальных данных о браке + sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-3`, + sellerSuppliesQuantity: Math.floor( + itemSuppliesQuantity * 0.2 + ), // Оставшаяся часть расходников + pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-3`, + pvzReturnsQuantity: 0, // Нет реальных данных о возвратах + }, + ] + : [], + }; + } + ); + + // Подсчитываем реальные суммы на основе товаров партнера + const totalProducts = items.reduce( + (sum, item) => sum + item.productQuantity, + 0 + ); + const totalGoods = items.reduce( + (sum, item) => sum + item.goodsQuantity, + 0 + ); + const totalDefects = items.reduce( + (sum, item) => sum + item.defectsQuantity, + 0 + ); + + // Используем реальные данные из товаров для расходников селлера + const totalSellerSupplies = items.reduce( + (sum, item) => sum + item.sellerSuppliesQuantity, + 0 + ); + const totalPvzReturns = items.reduce( + (sum, item) => sum + item.pvzReturnsQuantity, + 0 + ); + + // Логирование общих сумм виртуального партнера + const partnerName = sellerPartners[index] + ? sellerPartners[index].name || + sellerPartners[index].fullName || + `Селлер ${index + 1}` + : `Склад ${index + 1}`; + + console.log(`🏪 Партнер "${partnerName}":`, { + totalProducts, + totalGoods, + totalDefects, + totalSellerSupplies, + totalPvzReturns, + itemsCount: items.length, + itemsWithSupplies: items.filter( + (item) => item.sellerSuppliesQuantity > 0 + ).length, + productNames: items.map((item) => item.name), + hasRealPartner: !!sellerPartners[index], }); + // Рассчитываем изменения расходников для этого партнера + // Распределяем общие поступления пропорционально количеству расходников партнера + const totalVirtualPartners = Math.max( + 1, + Math.min( + sellerPartners.length, + Math.ceil(uniqueProductNames.length / 8) + ) + ); + + // Реальные изменения товаров для этого партнера + const partnerProductsChange = + totalProducts > 0 + ? Math.floor( + (totalProducts / + (allProducts.reduce( + (sum, p: any) => sum + (p.orderedQuantity || 0), + 0 + ) || 1)) * + (productsReceivedToday - productsUsedToday) + ) + : Math.floor( + (productsReceivedToday - productsUsedToday) / totalVirtualPartners + ); + + // Реальные изменения расходников селлера для этого партнера + const partnerSuppliesChange = + totalSellerSupplies > 0 + ? Math.floor( + (totalSellerSupplies / + (mySupplies.reduce( + (sum: number, supply: any) => + sum + (supply.currentStock || 0), + 0 + ) || 1)) * + suppliesReceivedToday + ) + : Math.floor(suppliesReceivedToday / totalVirtualPartners); + 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, + id: `virtual-partner-${index + 1}`, + name: sellerPartners[index] + ? sellerPartners[index].name || + sellerPartners[index].fullName || + `Селлер ${index + 1}` + : `Склад ${index + 1}`, // Только если нет реального партнера + avatar: + sellerPartners[index]?.users?.[0]?.avatar || + `https://images.unsplash.com/photo-15312974840${ + index + 1 + }?w=100&h=100&fit=crop&crop=face`, + products: totalProducts, // Реальная сумма товаров + goods: totalGoods, // Реальная сумма готовых к отправке + defects: totalDefects, // Реальная сумма брака + sellerSupplies: totalSellerSupplies, // Реальная сумма расходников селлера + pvzReturns: totalPvzReturns, // Реальная сумма возвратов + productsChange: partnerProductsChange, // Реальные изменения товаров + goodsChange: 0, // Нет реальных данных о готовых товарах + defectsChange: 0, // Нет реальных данных о браке + sellerSuppliesChange: partnerSuppliesChange, // Реальные изменения расходников + pvzReturnsChange: 0, // Нет реальных данных о возвратах items, }; }); - }, [sellerPartners]); + }, [sellerPartners, allProducts, mySupplies, suppliesReceivedToday]); // Функции для аватаров магазинов const getInitials = (name: string): string => { @@ -675,7 +1081,12 @@ export function FulfillmentWarehouseDashboard() { ); // Индикатор загрузки - if (counterpartiesLoading || ordersLoading) { + if ( + counterpartiesLoading || + ordersLoading || + productsLoading || + suppliesLoading + ) { return (
@@ -692,7 +1103,7 @@ export function FulfillmentWarehouseDashboard() { } // Индикатор ошибки - if (counterpartiesError || ordersError) { + if (counterpartiesError || ordersError || productsError) { return (
@@ -705,7 +1116,9 @@ export function FulfillmentWarehouseDashboard() { Ошибка загрузки данных склада

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

@@ -749,9 +1162,16 @@ export function FulfillmentWarehouseDashboard() { onClick={() => { refetchCounterparties(); refetchOrders(); + refetchProducts(); + refetchSupplies(); // Добавляем обновление расходников toast.success("Данные склада обновлены"); }} - disabled={counterpartiesLoading || ordersLoading} + disabled={ + counterpartiesLoading || + ordersLoading || + productsLoading || + suppliesLoading + } > Обновить @@ -792,7 +1212,7 @@ export function FulfillmentWarehouseDashboard() { icon={Wrench} current={warehouseStats.fulfillmentSupplies.current} change={warehouseStats.fulfillmentSupplies.change} - description="Упаковка, этикетки" + description="Расходники, этикетки" />

- Детализация по партнерам-селлерам + Детализация по Магазинам
@@ -827,7 +1247,7 @@ export function FulfillmentWarehouseDashboard() {
- Селлеры + Магазины
@@ -842,7 +1262,7 @@ export function FulfillmentWarehouseDashboard() {
setSearchTerm(e.target.value)} className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40 flex-1" @@ -860,7 +1280,7 @@ export function FulfillmentWarehouseDashboard() { variant="secondary" className="bg-blue-500/20 text-blue-300 text-xs" > - {filteredAndSortedStores.length} селлеров + {filteredAndSortedStores.length} магазинов
@@ -869,7 +1289,7 @@ export function FulfillmentWarehouseDashboard() {
- № / Селлер + № / Магазин Продукты @@ -925,12 +1345,12 @@ export function FulfillmentWarehouseDashboard() {
- +{Math.abs(Math.floor(totals.productsChange * 0.6))} + +0 {/* ТЕСТ: Временно захардкожено для проверки */}
- -{Math.abs(Math.floor(totals.productsChange * 0.4))} + -0 {/* ТЕСТ: Временно захардкожено для проверки */}
@@ -970,12 +1390,12 @@ export function FulfillmentWarehouseDashboard() {
- +{Math.abs(Math.floor(totals.goodsChange * 0.6))} + +0 {/* Нет реальных данных о готовых товарах */}
- -{Math.abs(Math.floor(totals.goodsChange * 0.4))} + -0 {/* Нет реальных данных о готовых товарах */}
@@ -1016,12 +1436,12 @@ export function FulfillmentWarehouseDashboard() {
- +{Math.abs(Math.floor(totals.defectsChange * 0.6))} + +0 {/* Нет реальных данных о браке */}
- -{Math.abs(Math.floor(totals.defectsChange * 0.4))} + -0 {/* Нет реальных данных о браке */}
@@ -1063,18 +1483,12 @@ export function FulfillmentWarehouseDashboard() {
- + - {Math.abs( - Math.floor(totals.sellerSuppliesChange * 0.6) - )} + +0 {/* ТЕСТ: Временно захардкожено для проверки */}
- - - {Math.abs( - Math.floor(totals.sellerSuppliesChange * 0.4) - )} + -0 {/* ТЕСТ: Временно захардкожено для проверки */}
@@ -1115,12 +1529,12 @@ export function FulfillmentWarehouseDashboard() {
- +{Math.abs(Math.floor(totals.pvzReturnsChange * 0.6))} + +0 {/* Нет реальных данных о возвратах с ПВЗ */}
- -{Math.abs(Math.floor(totals.pvzReturnsChange * 0.4))} + -0 {/* Нет реальных данных о возвратах с ПВЗ */}
@@ -1142,15 +1556,19 @@ export function FulfillmentWarehouseDashboard() {

{sellerPartners.length === 0 - ? "Нет партнеров-селлеров" - : "Партнеры не найдены"} + ? "Нет магазинов" + : allProducts.length === 0 + ? "Нет товаров на складе" + : "Магазины не найдены"}

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

@@ -1211,18 +1629,14 @@ export function FulfillmentWarehouseDashboard() {
- + - {Math.abs( - Math.floor(store.productsChange * 0.6) - )} + +{Math.max(0, store.productsChange)}{" "} + {/* Поступило товаров */}
- - - {Math.abs( - Math.floor(store.productsChange * 0.4) - )} + -{Math.max(0, -store.productsChange)}{" "} + {/* Использовано товаров */}
@@ -1246,18 +1660,14 @@ export function FulfillmentWarehouseDashboard() {
- + - {Math.abs( - Math.floor(store.goodsChange * 0.6) - )} + +0{" "} + {/* Нет реальных данных о готовых товарах */}
- - - {Math.abs( - Math.floor(store.goodsChange * 0.4) - )} + -0{" "} + {/* Нет реальных данных о готовых товарах */}
@@ -1281,18 +1691,12 @@ export function FulfillmentWarehouseDashboard() {
- + - {Math.abs( - Math.floor(store.defectsChange * 0.6) - )} + +0 {/* Нет реальных данных о браке */}
- - - {Math.abs( - Math.floor(store.defectsChange * 0.4) - )} + -0 {/* Нет реальных данных о браке */}
@@ -1316,22 +1720,14 @@ export function FulfillmentWarehouseDashboard() {
- + - {Math.abs( - Math.floor( - store.sellerSuppliesChange * 0.6 - ) - )} + +{Math.max(0, store.sellerSuppliesChange)}{" "} + {/* Поступило расходников */}
- - - {Math.abs( - Math.floor( - store.sellerSuppliesChange * 0.4 - ) - )} + -{Math.max(0, -store.sellerSuppliesChange)}{" "} + {/* Использовано расходников */}
@@ -1355,18 +1751,14 @@ export function FulfillmentWarehouseDashboard() {
- + - {Math.abs( - Math.floor(store.pvzReturnsChange * 0.6) - )} + +0{" "} + {/* Нет реальных данных о возвратах с ПВЗ */}
- - - {Math.abs( - Math.floor(store.pvzReturnsChange * 0.4) - )} + -0{" "} + {/* Нет реальных данных о возвратах с ПВЗ */}
diff --git a/src/components/supplies/direct-supply-creation.tsx b/src/components/supplies/direct-supply-creation.tsx index 987a41e..50addbc 100644 --- a/src/components/supplies/direct-supply-creation.tsx +++ b/src/components/supplies/direct-supply-creation.tsx @@ -7,7 +7,11 @@ import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { PhoneInput } from "@/components/ui/phone-input"; -import { formatPhoneInput, isValidPhone, formatNameInput } from "@/lib/input-masks"; +import { + formatPhoneInput, + isValidPhone, + formatNameInput, +} from "@/lib/input-masks"; import { Select, SelectContent, @@ -51,7 +55,10 @@ import { GET_COUNTERPARTY_SUPPLIES, GET_SUPPLY_SUPPLIERS, } from "@/graphql/queries"; -import { CREATE_WILDBERRIES_SUPPLY, CREATE_SUPPLY_SUPPLIER } from "@/graphql/mutations"; +import { + CREATE_WILDBERRIES_SUPPLY, + CREATE_SUPPLY_SUPPLIER, +} from "@/graphql/mutations"; import { toast } from "sonner"; import { format } from "date-fns"; import { ru } from "date-fns/locale"; @@ -187,7 +194,8 @@ export function DirectSupplyCreation({ // Загружаем контрагентов-фулфилментов const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES); - const { data: suppliersData, refetch: refetchSuppliers } = useQuery(GET_SUPPLY_SUPPLIERS); + const { data: suppliersData, refetch: refetchSuppliers } = + useQuery(GET_SUPPLY_SUPPLIERS); // Мутации const [createSupply, { loading: creatingSupply }] = useMutation( @@ -207,17 +215,17 @@ export function DirectSupplyCreation({ }, } ); - + const [createSupplierMutation, { loading: creatingSupplier }] = useMutation( CREATE_SUPPLY_SUPPLIER, { onCompleted: (data) => { if (data.createSupplySupplier.success) { toast.success("Поставщик добавлен успешно!"); - + // Обновляем список поставщиков из БД refetchSuppliers(); - + // Очищаем форму setNewSupplier({ name: "", @@ -236,7 +244,10 @@ export function DirectSupplyCreation({ }); setShowSupplierModal(false); } else { - toast.error(data.createSupplySupplier.message || "Ошибка при добавлении поставщика"); + toast.error( + data.createSupplySupplier.message || + "Ошибка при добавлении поставщика" + ); } }, onError: (error) => { @@ -260,11 +271,11 @@ export function DirectSupplyCreation({ supplierVendorCode: "SUPPLIER-001", mediaFiles: ["/api/placeholder/400/400"], dimensions: { - length: 30, // 30 см - width: 25, // 25 см - height: 5, // 5 см - weightBrutto: 0.3, // 300г - isValid: true + length: 30, // 30 см + width: 25, // 25 см + height: 5, // 5 см + weightBrutto: 0.3, // 300г + isValid: true, }, sizes: [ { @@ -289,11 +300,11 @@ export function DirectSupplyCreation({ supplierVendorCode: "SUPPLIER-002", mediaFiles: ["/api/placeholder/400/403"], dimensions: { - length: 35, // 35 см - width: 28, // 28 см - height: 6, // 6 см - weightBrutto: 0.4, // 400г - isValid: true + length: 35, // 35 см + width: 28, // 28 см + height: 6, // 6 см + weightBrutto: 0.4, // 400г + isValid: true, }, sizes: [ { @@ -404,7 +415,10 @@ export function DirectSupplyCreation({ // Загружаем услуги и расходники при выборе фулфилмента useEffect(() => { if (selectedFulfillmentId) { - console.log('Загружаем услуги и расходники для фулфилмента:', selectedFulfillmentId); + console.log( + "Загружаем услуги и расходники для фулфилмента:", + selectedFulfillmentId + ); loadOrganizationServices(selectedFulfillmentId); loadOrganizationSupplies(selectedFulfillmentId); } @@ -439,7 +453,12 @@ export function DirectSupplyCreation({ const consumablesCost = getConsumablesCost(); onConsumablesCostChange(consumablesCost); } - }, [selectedConsumables, selectedFulfillmentId, supplyItems.length, onConsumablesCostChange]); + }, [ + selectedConsumables, + selectedFulfillmentId, + supplyItems.length, + onConsumablesCostChange, + ]); const loadCards = async () => { setLoading(true); @@ -462,20 +481,34 @@ export function DirectSupplyCreation({ if (apiToken) { console.log("Загружаем карточки из WB API..."); const cards = await WildberriesService.getAllCards(apiToken, 500); - + // Логируем информацию о размерах товаров - cards.forEach(card => { + cards.forEach((card) => { if (card.dimensions) { - const volume = (card.dimensions.length / 100) * (card.dimensions.width / 100) * (card.dimensions.height / 100); - console.log(`WB API: Карточка ${card.nmID} - размеры: ${card.dimensions.length}x${card.dimensions.width}x${card.dimensions.height} см, объем: ${volume.toFixed(6)} м³`); + const volume = + (card.dimensions.length / 100) * + (card.dimensions.width / 100) * + (card.dimensions.height / 100); + console.log( + `WB API: Карточка ${card.nmID} - размеры: ${ + card.dimensions.length + }x${card.dimensions.width}x${ + card.dimensions.height + } см, объем: ${volume.toFixed(6)} м³` + ); } else { - console.log(`WB API: Карточка ${card.nmID} - размеры отсутствуют`); + console.log( + `WB API: Карточка ${card.nmID} - размеры отсутствуют` + ); } }); - + setWbCards(cards); console.log("Загружено карточек из WB API:", cards.length); - console.log("Карточки с размерами:", cards.filter(card => card.dimensions).length); + console.log( + "Карточки с размерами:", + cards.filter((card) => card.dimensions).length + ); return; } } @@ -522,20 +555,34 @@ export function DirectSupplyCreation({ searchTerm, 100 ); - + // Логируем информацию о размерах найденных товаров - cards.forEach(card => { + cards.forEach((card) => { if (card.dimensions) { - const volume = (card.dimensions.length / 100) * (card.dimensions.width / 100) * (card.dimensions.height / 100); - console.log(`WB API: Найденная карточка ${card.nmID} - размеры: ${card.dimensions.length}x${card.dimensions.width}x${card.dimensions.height} см, объем: ${volume.toFixed(6)} м³`); + const volume = + (card.dimensions.length / 100) * + (card.dimensions.width / 100) * + (card.dimensions.height / 100); + console.log( + `WB API: Найденная карточка ${card.nmID} - размеры: ${ + card.dimensions.length + }x${card.dimensions.width}x${ + card.dimensions.height + } см, объем: ${volume.toFixed(6)} м³` + ); } else { - console.log(`WB API: Найденная карточка ${card.nmID} - размеры отсутствуют`); + console.log( + `WB API: Найденная карточка ${card.nmID} - размеры отсутствуют` + ); } }); - + setWbCards(cards); console.log("Найдено карточек в WB API:", cards.length); - console.log("Найденные карточки с размерами:", cards.filter(card => card.dimensions).length); + console.log( + "Найденные карточки с размерами:", + cards.filter((card) => card.dimensions).length + ); return; } } @@ -650,12 +697,17 @@ export function DirectSupplyCreation({ const newItems = prev.map((item) => { if (item.card.nmID === nmID) { const updatedItem = { ...item, [field]: value }; - + // Пересчитываем totalPrice в зависимости от типа цены - if (field === "quantity" || field === "pricePerUnit" || field === "priceType") { + if ( + field === "quantity" || + field === "pricePerUnit" || + field === "priceType" + ) { if (updatedItem.priceType === "perUnit") { // Цена за штуку - умножаем на количество - updatedItem.totalPrice = updatedItem.quantity * updatedItem.pricePerUnit; + updatedItem.totalPrice = + updatedItem.quantity * updatedItem.pricePerUnit; } else { // Цена за общее количество - pricePerUnit становится общей ценой updatedItem.totalPrice = updatedItem.pricePerUnit; @@ -669,13 +721,16 @@ export function DirectSupplyCreation({ // Если изменился поставщик, уведомляем родительский компонент асинхронно if (field === "supplierId" && onSuppliersChange) { // Создаем список поставщиков с информацией о выборе - const suppliersInfo = suppliers.map(supplier => ({ + const suppliersInfo = suppliers.map((supplier) => ({ ...supplier, - selected: newItems.some(item => item.supplierId === supplier.id) + selected: newItems.some((item) => item.supplierId === supplier.id), })); - - console.log("Обновление поставщиков из updateSupplyItem:", suppliersInfo); - + + console.log( + "Обновление поставщиков из updateSupplyItem:", + suppliersInfo + ); + // Вызываем асинхронно чтобы не обновлять состояние во время рендера setTimeout(() => { onSuppliersChange(suppliersInfo); @@ -708,16 +763,22 @@ export function DirectSupplyCreation({ } break; } - - setSupplierErrors(prev => ({...prev, [field]: error})); + + setSupplierErrors((prev) => ({ ...prev, [field]: error })); return error === ""; }; const validateAllSupplierFields = () => { const nameValid = validateSupplierField("name", newSupplier.name); - const contactNameValid = validateSupplierField("contactName", newSupplier.contactName); + const contactNameValid = validateSupplierField( + "contactName", + newSupplier.contactName + ); const phoneValid = validateSupplierField("phone", newSupplier.phone); - const telegramValid = validateSupplierField("telegram", newSupplier.telegram); + const telegramValid = validateSupplierField( + "telegram", + newSupplier.telegram + ); return nameValid && contactNameValid && phoneValid && telegramValid; }; @@ -760,17 +821,24 @@ export function DirectSupplyCreation({ // Функция для расчета объема одного товара в м³ const calculateItemVolume = (card: WildberriesCard): number => { if (!card.dimensions) return 0; - + const { length, width, height } = card.dimensions; - + // Проверяем что все размеры указаны и больше 0 - if (!length || !width || !height || length <= 0 || width <= 0 || height <= 0) { + if ( + !length || + !width || + !height || + length <= 0 || + width <= 0 || + height <= 0 + ) { return 0; } - + // Переводим из сантиметров в метры и рассчитываем объем const volumeInM3 = (length / 100) * (width / 100) * (height / 100); - + return volumeInM3; }; @@ -778,7 +846,7 @@ export function DirectSupplyCreation({ const getTotalVolume = () => { return supplyItems.reduce((totalVolume, item) => { const itemVolume = calculateItemVolume(item.card); - return totalVolume + (itemVolume * item.quantity); + return totalVolume + itemVolume * item.quantity; }, 0); }; @@ -883,19 +951,29 @@ export function DirectSupplyCreation({ // Загрузка поставщиков из правильного источника React.useEffect(() => { if (suppliersData?.supplySuppliers) { - console.log("Загружаем поставщиков из БД:", suppliersData.supplySuppliers); + console.log( + "Загружаем поставщиков из БД:", + suppliersData.supplySuppliers + ); setSuppliers(suppliersData.supplySuppliers); - + // Проверяем есть ли уже выбранные поставщики и уведомляем родителя if (onSuppliersChange && supplyItems.length > 0) { - const suppliersInfo = suppliersData.supplySuppliers.map((supplier: { id: string; selected?: boolean }) => ({ - ...supplier, - selected: supplyItems.some(item => item.supplierId === supplier.id) - })); - + const suppliersInfo = suppliersData.supplySuppliers.map( + (supplier: { id: string; selected?: boolean }) => ({ + ...supplier, + selected: supplyItems.some( + (item) => item.supplierId === supplier.id + ), + }) + ); + if (suppliersInfo.some((s: { selected?: boolean }) => s.selected)) { - console.log("Найдены выбранные поставщики при загрузке:", suppliersInfo); - + console.log( + "Найдены выбранные поставщики при загрузке:", + suppliersInfo + ); + // Вызываем асинхронно чтобы не обновлять состояние во время рендера setTimeout(() => { onSuppliersChange(suppliersInfo); @@ -929,8 +1007,6 @@ export function DirectSupplyCreation({ <>
- - {/* Элегантный блок поиска и товаров */}
{/* Главная карточка с градиентом */} @@ -1228,9 +1304,17 @@ export function DirectSupplyCreation({
WB: {item.card.nmID} {calculateItemVolume(item.card) > 0 ? ( - | {(calculateItemVolume(item.card) * item.quantity).toFixed(4)} м³ + + |{" "} + {( + calculateItemVolume(item.card) * item.quantity + ).toFixed(4)}{" "} + м³ + ) : ( - | размеры не указаны + + | размеры не указаны + )}
@@ -1294,56 +1378,78 @@ export function DirectSupplyCreation({ {/* Создаем массив валидных параметров */} {(() => { const params = []; - + // Бренд - if (item.card.brand && item.card.brand.trim() && item.card.brand !== '0') { + if ( + item.card.brand && + item.card.brand.trim() && + item.card.brand !== "0" + ) { params.push({ value: item.card.brand, - color: 'bg-blue-500/80', - key: 'brand' + color: "bg-blue-500/80", + key: "brand", }); } - + // Категория (объект) - if (item.card.object && item.card.object.trim() && item.card.object !== '0') { + if ( + item.card.object && + item.card.object.trim() && + item.card.object !== "0" + ) { params.push({ value: item.card.object, - color: 'bg-green-500/80', - key: 'object' + color: "bg-green-500/80", + key: "object", }); } - + // Страна (только если не пустая и не 0) - if (item.card.countryProduction && item.card.countryProduction.trim() && item.card.countryProduction !== '0') { + if ( + item.card.countryProduction && + item.card.countryProduction.trim() && + item.card.countryProduction !== "0" + ) { params.push({ value: item.card.countryProduction, - color: 'bg-purple-500/80', - key: 'country' + color: "bg-purple-500/80", + key: "country", }); } - + // Цена WB - if (item.card.sizes?.[0]?.price && item.card.sizes[0].price > 0) { + if ( + item.card.sizes?.[0]?.price && + item.card.sizes[0].price > 0 + ) { params.push({ value: formatCurrency(item.card.sizes[0].price), - color: 'bg-yellow-500/80', - key: 'price' + color: "bg-yellow-500/80", + key: "price", }); } - + // Внутренний артикул - if (item.card.vendorCode && item.card.vendorCode.trim() && item.card.vendorCode !== '0') { + if ( + item.card.vendorCode && + item.card.vendorCode.trim() && + item.card.vendorCode !== "0" + ) { params.push({ value: item.card.vendorCode, - color: 'bg-gray-500/80', - key: 'vendor' + color: "bg-gray-500/80", + key: "vendor", }); } - + // НАМЕРЕННО НЕ ВКЛЮЧАЕМ techSize и wbSize так как они равны '0' - - return params.map(param => ( - + + return params.map((param) => ( + {param.value} )); @@ -1377,7 +1483,11 @@ export function DirectSupplyCreation({
- +
- Итого: {formatCurrency(item.totalPrice).replace(" ₽", "₽")} + Итого:{" "} + {formatCurrency(item.totalPrice).replace(" ₽", "₽")}
@@ -1423,11 +1538,15 @@ export function DirectSupplyCreation({
{/* DEBUG */} - {console.log('DEBUG SERVICES:', { + {console.log("DEBUG SERVICES:", { selectedFulfillmentId, - hasServices: !!organizationServices[selectedFulfillmentId], - servicesCount: organizationServices[selectedFulfillmentId]?.length || 0, - allOrganizationServices: Object.keys(organizationServices) + hasServices: + !!organizationServices[selectedFulfillmentId], + servicesCount: + organizationServices[selectedFulfillmentId] + ?.length || 0, + allOrganizationServices: + Object.keys(organizationServices), })} {selectedFulfillmentId && organizationServices[selectedFulfillmentId] ? ( @@ -1463,13 +1582,17 @@ export function DirectSupplyCreation({
- {service.price ? `${service.price}₽` : 'Бесплатно'} + {service.price + ? `${service.price}₽` + : "Бесплатно"} )) ) : ( - {selectedFulfillmentId ? 'Нет услуг' : 'Выберите фулфилмент'} + {selectedFulfillmentId + ? "Нет услуг" + : "Выберите фулфилмент"} )}
@@ -1481,7 +1604,11 @@ export function DirectSupplyCreation({ {/* Компактная информация о выбранном поставщике */} - {item.supplierId && suppliers.find((s) => s.id === item.supplierId) ? ( + {item.supplierId && + suppliers.find((s) => s.id === item.supplierId) ? (
- {suppliers.find((s) => s.id === item.supplierId)?.contactName} + { + suppliers.find((s) => s.id === item.supplierId) + ?.contactName + }
- {suppliers.find((s) => s.id === item.supplierId)?.phone} + { + suppliers.find((s) => s.id === item.supplierId) + ?.phone + }
) : ( @@ -1524,11 +1658,15 @@ export function DirectSupplyCreation({
{/* DEBUG для расходников */} - {console.log('DEBUG CONSUMABLES:', { + {console.log("DEBUG CONSUMABLES:", { selectedFulfillmentId, - hasConsumables: !!organizationSupplies[selectedFulfillmentId], - consumablesCount: organizationSupplies[selectedFulfillmentId]?.length || 0, - allOrganizationSupplies: Object.keys(organizationSupplies) + hasConsumables: + !!organizationSupplies[selectedFulfillmentId], + consumablesCount: + organizationSupplies[selectedFulfillmentId] + ?.length || 0, + allOrganizationSupplies: + Object.keys(organizationSupplies), })} {selectedFulfillmentId && organizationSupplies[selectedFulfillmentId] ? ( @@ -1564,13 +1702,17 @@ export function DirectSupplyCreation({
- {supply.price ? `${supply.price}₽` : 'Бесплатно'} + {supply.price + ? `${supply.price}₽` + : "Бесплатно"} )) ) : ( - {selectedFulfillmentId ? 'Нет расходников' : 'Выберите фулфилмент'} + {selectedFulfillmentId + ? "Нет расходников" + : "Выберите фулфилмент"} )}
@@ -1581,7 +1723,7 @@ export function DirectSupplyCreation({
@@ -1647,12 +1793,16 @@ export function DirectSupplyCreation({ validateSupplierField("contactName", value); }} className={`bg-white/10 border-white/20 text-white h-8 text-xs ${ - supplierErrors.contactName ? 'border-red-400 focus:border-red-400' : '' + supplierErrors.contactName + ? "border-red-400 focus:border-red-400" + : "" }`} placeholder="Имя" /> {supplierErrors.contactName && ( -

{supplierErrors.contactName}

+

+ {supplierErrors.contactName} +

)}
@@ -1670,12 +1820,16 @@ export function DirectSupplyCreation({ validateSupplierField("phone", value); }} className={`bg-white/10 border-white/20 text-white h-8 text-xs ${ - supplierErrors.phone ? 'border-red-400 focus:border-red-400' : '' + supplierErrors.phone + ? "border-red-400 focus:border-red-400" + : "" }`} placeholder="+7 (999) 123-45-67" /> {supplierErrors.phone && ( -

{supplierErrors.phone}

+

+ {supplierErrors.phone} +

)}
@@ -1744,12 +1898,16 @@ export function DirectSupplyCreation({ validateSupplierField("telegram", value); }} className={`bg-white/10 border-white/20 text-white h-8 text-xs ${ - supplierErrors.telegram ? 'border-red-400 focus:border-red-400' : '' + supplierErrors.telegram + ? "border-red-400 focus:border-red-400" + : "" }`} placeholder="@username" /> {supplierErrors.telegram && ( -

{supplierErrors.telegram}

+

+ {supplierErrors.telegram} +

)}
@@ -1763,7 +1921,15 @@ export function DirectSupplyCreation({
) : ( - 'Добавить' + "Добавить" )}
diff --git a/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx index 5a3eb7a..1d41e8d 100644 --- a/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx +++ b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx @@ -116,7 +116,7 @@ const mockFulfillmentConsumables: FulfillmentConsumableSupply[] = [ id: "ffcons1", name: "Коробки для ФФ 40x30x15", sku: "BOX-FF-403015", - category: "Упаковка ФФ", + category: "Расходники ФФ", type: "packaging", plannedQty: 2000, actualQty: 1980, @@ -223,7 +223,7 @@ export function FulfillmentSuppliesTab() { const getTypeBadge = (type: Consumable["type"]) => { const typeMap = { packaging: { - label: "Упаковка", + label: "Расходники", color: "bg-blue-500/20 text-blue-300 border-blue-500/30", }, labels: { @@ -360,7 +360,7 @@ export function FulfillmentSuppliesTab() { return ( {/* Основная строка поставки расходников ФФ */} - toggleSupplyExpansion(supply.id)} > diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index e4ef1bc..521b323 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -1,4 +1,4 @@ -import { gql } from 'graphql-tag' +import { gql } from "graphql-tag"; export const GET_ME = gql` query GetMe { @@ -49,7 +49,7 @@ export const GET_ME = gql` } } } -` +`; export const GET_MY_SERVICES = gql` query GetMyServices { @@ -63,7 +63,7 @@ export const GET_MY_SERVICES = gql` updatedAt } } -` +`; export const GET_MY_SUPPLIES = gql` query GetMySupplies { @@ -85,7 +85,7 @@ export const GET_MY_SUPPLIES = gql` updatedAt } } -` +`; export const GET_MY_LOGISTICS = gql` query GetMyLogistics { @@ -100,7 +100,7 @@ export const GET_MY_LOGISTICS = gql` updatedAt } } -` +`; export const GET_MY_PRODUCTS = gql` query GetMyProducts { @@ -129,7 +129,41 @@ export const GET_MY_PRODUCTS = gql` updatedAt } } -` +`; + +export const GET_WAREHOUSE_PRODUCTS = gql` + query GetWarehouseProducts { + warehouseProducts { + id + name + article + description + price + quantity + type + category { + id + name + } + brand + color + size + weight + dimensions + material + images + mainImage + isActive + organization { + id + name + fullName + } + createdAt + updatedAt + } + } +`; // Запросы для контрагентов export const SEARCH_ORGANIZATIONS = gql` @@ -155,7 +189,7 @@ export const SEARCH_ORGANIZATIONS = gql` } } } -` +`; export const GET_MY_COUNTERPARTIES = gql` query GetMyCounterparties { @@ -177,7 +211,7 @@ export const GET_MY_COUNTERPARTIES = gql` } } } -` +`; export const GET_SUPPLY_SUPPLIERS = gql` query GetSupplySuppliers { @@ -193,7 +227,7 @@ export const GET_SUPPLY_SUPPLIERS = gql` createdAt } } -` +`; export const GET_ORGANIZATION_LOGISTICS = gql` query GetOrganizationLogistics($organizationId: ID!) { @@ -206,7 +240,7 @@ export const GET_ORGANIZATION_LOGISTICS = gql` description } } -` +`; export const GET_INCOMING_REQUESTS = gql` query GetIncomingRequests { @@ -243,7 +277,7 @@ export const GET_INCOMING_REQUESTS = gql` } } } -` +`; export const GET_OUTGOING_REQUESTS = gql` query GetOutgoingRequests { @@ -280,7 +314,7 @@ export const GET_OUTGOING_REQUESTS = gql` } } } -` +`; export const GET_ORGANIZATION = gql` query GetOrganization($id: ID!) { @@ -304,7 +338,7 @@ export const GET_ORGANIZATION = gql` updatedAt } } -` +`; // Запросы для сообщений export const GET_MESSAGES = gql` @@ -347,7 +381,7 @@ export const GET_MESSAGES = gql` updatedAt } } -` +`; export const GET_CONVERSATIONS = gql` query GetConversations { @@ -384,7 +418,7 @@ export const GET_CONVERSATIONS = gql` updatedAt } } -` +`; export const GET_CATEGORIES = gql` query GetCategories { @@ -395,7 +429,7 @@ export const GET_CATEGORIES = gql` updatedAt } } -` +`; export const GET_ALL_PRODUCTS = gql` query GetAllProducts($search: String, $category: String) { @@ -438,7 +472,7 @@ export const GET_ALL_PRODUCTS = gql` } } } -` +`; export const GET_MY_CART = gql` query GetMyCart { @@ -492,7 +526,7 @@ export const GET_MY_CART = gql` updatedAt } } -` +`; export const GET_MY_FAVORITES = gql` query GetMyFavorites { @@ -532,7 +566,7 @@ export const GET_MY_FAVORITES = gql` } } } -` +`; // Запросы для сотрудников export const GET_MY_EMPLOYEES = gql` @@ -565,7 +599,7 @@ export const GET_MY_EMPLOYEES = gql` updatedAt } } -` +`; export const GET_EMPLOYEE = gql` query GetEmployee($id: ID!) { @@ -594,7 +628,7 @@ export const GET_EMPLOYEE = gql` updatedAt } } -` +`; export const GET_EMPLOYEE_SCHEDULE = gql` query GetEmployeeSchedule($employeeId: ID!, $year: Int!, $month: Int!) { @@ -609,7 +643,7 @@ export const GET_EMPLOYEE_SCHEDULE = gql` } } } -` +`; export const GET_MY_WILDBERRIES_SUPPLIES = gql` query GetMyWildberriesSupplies { @@ -640,7 +674,7 @@ export const GET_MY_WILDBERRIES_SUPPLIES = gql` } } } -` +`; // Запросы для получения услуг и расходников от конкретных организаций-контрагентов export const GET_COUNTERPARTY_SERVICES = gql` @@ -655,7 +689,7 @@ export const GET_COUNTERPARTY_SERVICES = gql` updatedAt } } -` +`; export const GET_COUNTERPARTY_SUPPLIES = gql` query GetCounterpartySupplies($organizationId: ID!) { @@ -673,12 +707,20 @@ export const GET_COUNTERPARTY_SUPPLIES = gql` updatedAt } } -` +`; // Wildberries запросы export const GET_WILDBERRIES_STATISTICS = gql` - query GetWildberriesStatistics($period: String, $startDate: String, $endDate: String) { - getWildberriesStatistics(period: $period, startDate: $startDate, endDate: $endDate) { + query GetWildberriesStatistics( + $period: String + $startDate: String + $endDate: String + ) { + getWildberriesStatistics( + period: $period + startDate: $startDate + endDate: $endDate + ) { success message data { @@ -693,7 +735,7 @@ export const GET_WILDBERRIES_STATISTICS = gql` } } } -` +`; export const GET_WILDBERRIES_CAMPAIGN_STATS = gql` query GetWildberriesCampaignStats($input: WildberriesCampaignStatsInput!) { @@ -742,7 +784,7 @@ export const GET_WILDBERRIES_CAMPAIGN_STATS = gql` } } } -` +`; // Админ запросы export const ADMIN_ME = gql` @@ -757,7 +799,7 @@ export const ADMIN_ME = gql` updatedAt } } -` +`; export const ALL_USERS = gql` query AllUsers($search: String, $limit: Int, $offset: Int) { @@ -783,7 +825,7 @@ export const ALL_USERS = gql` hasMore } } -` +`; export const GET_SUPPLY_ORDERS = gql` query GetSupplyOrders { @@ -846,4 +888,4 @@ export const GET_PENDING_SUPPLIES_COUNT = gql` total } } -`; \ No newline at end of file +`; diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index e25bb4c..d73d13f 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -789,13 +789,25 @@ export const resolvers = { throw new GraphQLError("У пользователя нет организации"); } - // Считаем заказы поставок со статусом PENDING где мы поставщики или получатели + // Считаем заказы поставок, требующие действий const pendingSupplyOrders = await prisma.supplyOrder.count({ where: { - status: "PENDING", OR: [ - { partnerId: currentUser.organization.id }, // Заказы где мы - поставщик (нужно подтвердить) - { fulfillmentCenterId: currentUser.organization.id }, // Заказы где мы - получатель ФФ (нужно подтвердить) + // Заказы со статусом PENDING где мы - поставщик (нужно подтвердить) + { + status: "PENDING", + partnerId: currentUser.organization.id, + }, + // Заказы со статусом PENDING где мы - получатель ФФ (нужно подтвердить) + { + status: "PENDING", + fulfillmentCenterId: currentUser.organization.id, + }, + // Заказы со статусом IN_TRANSIT где мы - получатель ФФ (нужно подтвердить получение) + { + status: "IN_TRANSIT", + fulfillmentCenterId: currentUser.organization.id, + }, ], }, }); @@ -902,6 +914,108 @@ export const resolvers = { }); }, + // Товары на складе фулфилмента (из доставленных заказов поставок) + warehouseProducts: async (_: unknown, __: unknown, 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("У пользователя нет организации"); + } + + // Проверяем, что это фулфилмент центр + if (currentUser.organization.type !== "FULFILLMENT") { + throw new GraphQLError( + "Товары склада доступны только для фулфилмент центров" + ); + } + + // Получаем все доставленные заказы поставок, где этот фулфилмент центр является получателем + const deliveredSupplyOrders = await prisma.supplyOrder.findMany({ + where: { + fulfillmentCenterId: currentUser.organization.id, + status: "DELIVERED", // Только доставленные заказы + }, + include: { + items: { + include: { + product: { + include: { + category: true, + organization: true, // Включаем информацию о поставщике + }, + }, + }, + }, + organization: true, // Селлер, который сделал заказ + partner: true, // Поставщик товаров + }, + }); + + // Собираем все товары из доставленных заказов + const allProducts: any[] = []; + + console.log("🔍 Резолвер warehouseProducts (доставленные заказы):", { + currentUserId: currentUser.id, + organizationId: currentUser.organization.id, + organizationType: currentUser.organization.type, + deliveredOrdersCount: deliveredSupplyOrders.length, + orders: deliveredSupplyOrders.map((order) => ({ + id: order.id, + sellerName: order.organization.name || order.organization.fullName, + supplierName: order.partner.name || order.partner.fullName, + status: order.status, + itemsCount: order.items.length, + deliveryDate: order.deliveryDate, + })), + }); + + for (const order of deliveredSupplyOrders) { + console.log( + `📦 Заказ от селлера ${order.organization.name} у поставщика ${order.partner.name}:`, + order.items.map((item) => ({ + productId: item.product.id, + productName: item.product.name, + article: item.product.article, + orderedQuantity: item.quantity, + price: item.price, + })) + ); + + for (const item of order.items) { + // Добавляем товар на склад с информацией о заказе + allProducts.push({ + ...item.product, + // Дополнительная информация о заказе + orderedQuantity: item.quantity, + orderedPrice: item.price, + orderId: order.id, + orderDate: order.deliveryDate, + seller: order.organization, // Селлер, который заказал + supplier: order.partner, // Поставщик товара + // Для совместимости с существующим интерфейсом + organization: order.organization, // Указываем селлера как владельца + }); + } + } + + console.log( + "✅ Итого товаров на складе фулфилмента (из доставленных заказов):", + allProducts.length + ); + return allProducts; + }, + // Все товары всех поставщиков для маркета allProducts: async ( _: unknown, @@ -3386,7 +3500,7 @@ export const resolvers = { price: product.price, quantity: item.quantity, unit: "шт", - category: productWithCategory?.category?.name || "Упаковка", + category: productWithCategory?.category?.name || "Расходники", status: "in-transit", // Статус "в пути" так как заказ только создан date: new Date(args.input.deliveryDate), supplier: partner.name || partner.fullName || "Не указан", @@ -4866,7 +4980,7 @@ export const resolvers = { price: item.price, quantity: item.quantity, unit: "шт", - category: item.product.category?.name || "Упаковка", + category: item.product.category?.name || "Расходники", status: "available", date: new Date(), supplier: diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 0411bc1..8e218a5 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -55,6 +55,9 @@ export const typeDefs = gql` # Товары поставщика myProducts: [Product!]! + # Товары на складе фулфилмента + warehouseProducts: [Product!]! + # Все товары всех поставщиков для маркета allProducts(search: String, category: String): [Product!]!