From b529faa5164b178db2e0f905bc8b7ed0458895c1 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Mon, 28 Jul 2025 16:15:52 +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=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BA=D0=B0=D0=BC=D0=B8:=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2=20=D1=80=D0=B0=D1=81=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=D1=81=D0=B5=D0=BB?= =?UTF-8?q?=D0=BB=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B8=20=D0=BD=D0=B0=D1=88?= =?UTF-8?q?=D0=B8=D1=85=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5?= =?UTF-8?q?=20GraphQL=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D0=B7=D0=BE=D0=BB=D0=B2=D0=B5=D1=80=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D0=BE=D0=B6=D0=B8=D0=B4?= =?UTF-8?q?=D0=B0=D1=8E=D1=89=D0=B8=D0=BC=20=D0=BF=D0=BE=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=D0=BC.=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=83=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=B2=20=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fulfillment-consumables-orders-tab.tsx | 8 +- .../fulfillment-detailed-supplies-tab.tsx | 185 ++++++++++--- .../fulfillment-supplies-tab.tsx | 9 +- .../seller-materials-tab.tsx | 249 +++++++++++------- src/graphql/queries.ts | 4 +- src/graphql/resolvers.ts | 52 ++-- src/graphql/typedefs.ts | 2 + 7 files changed, 344 insertions(+), 165 deletions(-) diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx index 059b8d9..dd7b65b 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx @@ -170,15 +170,17 @@ export function FulfillmentConsumablesOrdersTab() { // Получаем данные заказов поставок const supplyOrders: SupplyOrder[] = data?.supplyOrders || []; - // Фильтруем заказы для фулфилмента (где текущий фулфилмент является получателем) + // Фильтруем заказы для фулфилмента (расходники селлеров) const fulfillmentOrders = supplyOrders.filter((order) => { // Показываем только заказы где текущий фулфилмент-центр является получателем const isRecipient = order.fulfillmentCenter?.id === user?.organization?.id; - // И статус не PENDING и не CANCELLED (одобренные заявки) + // НО создатель заказа НЕ мы (т.е. селлер создал заказ для нас) + const isCreatedByOther = order.organization?.id !== user?.organization?.id; + // И статус не PENDING и не CANCELLED (одобренные поставщиком заявки) const isApproved = order.status !== "CANCELLED" && order.status !== "PENDING"; - return isRecipient && isApproved; + return isRecipient && isCreatedByOther && isApproved; }); // Генерируем порядковые номера для заказов 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 3701f5b..7218992 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 @@ -7,12 +7,14 @@ import { Badge } from "@/components/ui/badge"; 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 { useQuery, useMutation } from "@apollo/client"; import { GET_SUPPLY_ORDERS, GET_PENDING_SUPPLIES_COUNT, } from "@/graphql/queries"; +import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations"; import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; import { Calendar, Building2, @@ -25,6 +27,8 @@ import { ChevronRight, Bell, AlertTriangle, + Truck, + CheckCircle, } from "lucide-react"; // Компонент уведомлений о непринятых поставках @@ -36,8 +40,10 @@ function PendingSuppliesAlert() { }); const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; - const supplyOrdersCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0; - const incomingRequestsCount = pendingData?.pendingSuppliesCount?.incomingRequests || 0; + const supplyOrdersCount = + pendingData?.pendingSuppliesCount?.supplyOrders || 0; + const incomingRequestsCount = + pendingData?.pendingSuppliesCount?.incomingRequests || 0; if (pendingCount === 0) return null; @@ -52,14 +58,19 @@ function PendingSuppliesAlert() { Требует вашего внимания -
- {supplyOrdersCount > 0 && ( -

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

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

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

- )} -
+
+ {supplyOrdersCount > 0 && ( +

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

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

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

+ )} +
@@ -80,6 +91,18 @@ interface SupplyOrder { totalItems: number; totalAmount: number; status: string; + fulfillmentCenterId: string; + organization: { + id: string; + name?: string; + fullName?: string; + type: string; + }; + partner: { + id: string; + name?: string; + fullName?: string; + }; items: { id: string; quantity: number; @@ -120,12 +143,8 @@ const getStatusBadge = (status: string) => { 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: "Отправлен", + IN_TRANSIT: { + label: "В пути", color: "bg-orange-500/20 text-orange-300 border-orange-500/30", }, DELIVERED: { @@ -149,6 +168,15 @@ export function FulfillmentDetailedSuppliesTab() { const { user } = useAuth(); const [expandedOrders, setExpandedOrders] = useState>(new Set()); + // Мутация для обновления статуса заказа + const [updateSupplyOrderStatus] = useMutation(UPDATE_SUPPLY_ORDER_STATUS, { + refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + onError: (error) => { + console.error("Error updating supply order status:", error); + toast.error("Ошибка при обновлении статуса заказа"); + }, + }); + // Загружаем реальные данные заказов расходников const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, { fetchPolicy: "cache-and-network", // Принудительно проверяем сервер @@ -158,11 +186,19 @@ export function FulfillmentDetailedSuppliesTab() { // Получаем ID текущей организации (фулфилмент-центра) const currentOrganizationId = user?.organization?.id; - // Фильтруем заказы созданные текущей организацией (наши расходники) + // "Наши расходники" = расходники, которые МЫ (фулфилмент-центр) заказали для себя + // Критерии: создатель = мы И получатель = мы (ОБА условия) const ourSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( - (order: SupplyOrder) => order.organizationId === currentOrganizationId + (order: SupplyOrder) => { + return ( + order.organizationId === currentOrganizationId && // Создали мы + order.fulfillmentCenterId === currentOrganizationId // Получатель - мы + ); + } ); + // Убираем разделение на createdByUs и createdForUs, так как здесь только наши поставки + const toggleOrderExpansion = (orderId: string) => { const newExpanded = new Set(expandedOrders); if (newExpanded.has(orderId)) { @@ -173,6 +209,31 @@ export function FulfillmentDetailedSuppliesTab() { setExpandedOrders(newExpanded); }; + // Функция для обновления статуса заказа + const handleStatusUpdate = async (orderId: string, newStatus: string) => { + try { + await updateSupplyOrderStatus({ + variables: { + id: orderId, + status: newStatus, + }, + }); + toast.success("Статус заказа обновлен"); + } catch (error) { + console.error("Error updating status:", error); + } + }; + + // Проверяем, можно ли отметить как доставленный + const canMarkAsDelivered = (status: string) => { + return status === "IN_TRANSIT"; + }; + + // Проверяем, можно ли отметить как в пути + const canMarkAsInTransit = (status: string) => { + return status === "CONFIRMED"; + }; + if (loading) { return (
@@ -202,13 +263,13 @@ export function FulfillmentDetailedSuppliesTab() {
{/* Уведомления о непринятых поставках */} - + {/* Заголовок с кнопкой создания поставки */}

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

- Управление поставками расходников фулфилмента + Поставки расходников, поступающие на склад фулфилмент-центра

- {getStatusBadge(order.status)} + +
+ {getStatusBadge(order.status)} + + {/* Кнопка "В пути" для подтвержденных заказов */} + {canMarkAsInTransit(order.status) && ( + + )} + + {/* Кнопка "Получено" для заказов в пути */} + {canMarkAsDelivered(order.status) && ( + + )} +
+ {/* Развернутая информация о заказе */} {isOrderExpanded && ( - +
+
+
+ + + Дата создания:{" "} + {formatDate(order.createdAt)} + +
+
+ + + Поставщик:{" "} + {order.partner.name || + order.partner.fullName} + +
+

Состав заказа:

diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx index 4be6b05..d98f3cf 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx @@ -39,6 +39,10 @@ export function FulfillmentSuppliesTab() { }); const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; + const ourSupplyOrdersCount = + pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0; + const sellerSupplyOrdersCount = + pendingData?.pendingSuppliesCount?.sellerSupplyOrders || 0; // Проверяем URL параметр при загрузке useEffect(() => { @@ -79,12 +83,13 @@ export function FulfillmentSuppliesTab() { Наши расходники Наши Н + Расходники селлеров Селлеры С - + { + return ( + order.organizationId !== currentOrganizationId && // Создали НЕ мы (селлер) + order.fulfillmentCenterId === currentOrganizationId // Получатель - мы + ); + }); + const formatCurrency = (amount: number) => { return new Intl.NumberFormat("ru-RU", { style: "currency", @@ -76,58 +99,83 @@ export function SellerMaterialsTab() { const getStatusBadge = (status: string) => { const statusConfig = { - planned: { + PENDING: { color: "text-blue-300 border-blue-400/30", - label: "Запланировано", + label: "Ожидает подтверждения", }, - "in-transit": { + CONFIRMED: { color: "text-yellow-300 border-yellow-400/30", + label: "Подтверждено", + }, + IN_TRANSIT: { + color: "text-orange-300 border-orange-400/30", label: "В пути", }, - delivered: { + DELIVERED: { color: "text-green-300 border-green-400/30", label: "Доставлено", }, - "in-processing": { - color: "text-purple-300 border-purple-400/30", - label: "Обрабатывается", + CANCELLED: { + color: "text-red-300 border-red-400/30", + label: "Отменено", }, }; const config = - statusConfig[status as keyof typeof statusConfig] || statusConfig.planned; - - return ( - - {config.label} - - ); + statusConfig[status as keyof typeof statusConfig] || statusConfig.PENDING; + return {config.label}; }; - const filteredMaterials = mockSellerMaterials.filter((material) => { + // Фильтрация поставок + const filteredOrders = sellerSupplyOrders.filter((order) => { const matchesSearch = - material.materialName.toLowerCase().includes(searchTerm.toLowerCase()) || - material.seller.toLowerCase().includes(searchTerm.toLowerCase()) || - material.category.toLowerCase().includes(searchTerm.toLowerCase()); + order.organization.name + ?.toLowerCase() + .includes(searchTerm.toLowerCase()) || + order.organization.fullName + ?.toLowerCase() + .includes(searchTerm.toLowerCase()) || + order.items.some((item) => + item.product.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); const matchesStatus = - statusFilter === "all" || material.status === statusFilter; + statusFilter === "all" || order.status === statusFilter; return matchesSearch && matchesStatus; }); - const getTotalValue = () => { - return filteredMaterials.reduce( - (sum, material) => sum + material.totalValue, - 0 + if (loading) { + return ( +
+
+ + Загрузка расходников селлеров... + +
); + } + + if (error) { + return ( +
+
+ +

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

+

{error.message}

+
+
+ ); + } + + const getTotalValue = () => { + return filteredOrders.reduce((sum, order) => sum + order.totalAmount, 0); }; const getTotalQuantity = () => { - return filteredMaterials.reduce( - (sum, material) => sum + material.quantity, - 0 - ); + return filteredOrders.reduce((sum, order) => sum + order.totalItems, 0); }; return ( @@ -143,7 +191,7 @@ export function SellerMaterialsTab() {

Поставок

- {filteredMaterials.length} + {filteredOrders.length}

@@ -205,49 +253,67 @@ export function SellerMaterialsTab() { className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10" > - - - - + + + + +
{/* Список материалов */}
-
- {filteredMaterials.map((material) => ( + {filteredOrders.length === 0 ? ( + +
+ +

+ Нет поставок от селлеров +

+

+ Здесь будут отображаться расходники, которые селлеры заказывают для доставки на ваш склад. +

+
+
+ ) : ( +
+ {filteredOrders.map((order) => (

- {material.materialName} + {order.organization.name || order.organization.fullName}

- {getStatusBadge(material.status)} + {getStatusBadge(order.status)}

Селлер

-

{material.seller}

+

+ {order.organization.name || order.organization.fullName} +

-

Категория

-

{material.category}

+

Дата доставки

+

+ {formatDate(order.deliveryDate)} +

Количество

- {material.quantity.toLocaleString()} шт. + {order.totalItems.toLocaleString()} шт.

-

Ожидается

-

- {formatDate(material.expectedDate)} +

Общая стоимость

+

+ {formatCurrency(order.totalAmount)}

@@ -255,15 +321,9 @@ export function SellerMaterialsTab() {
- Цена за ед.:{" "} + Дата заказа:{" "} - {formatCurrency(material.unitPrice)} - - - - Общая стоимость:{" "} - - {formatCurrency(material.totalValue)} + {formatDate(order.createdAt)}
@@ -271,8 +331,8 @@ export function SellerMaterialsTab() {

- Назначение:{" "} - {material.purpose} + Статус:{" "} + {order.status}

@@ -289,7 +349,8 @@ export function SellerMaterialsTab() {
))} -
+
+ )}
); diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 40db734..661a6de 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -827,7 +827,7 @@ export const GET_WILDBERRIES_CAMPAIGNS_LIST = gql` } } } -` +`; // Админ запросы export const ADMIN_ME = gql` @@ -927,6 +927,8 @@ export const GET_PENDING_SUPPLIES_COUNT = gql` query GetPendingSuppliesCount { pendingSuppliesCount { supplyOrders + ourSupplyOrders + sellerSupplyOrders incomingRequests total } diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 11e1f9b..4f22ad8 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -790,28 +790,28 @@ export const resolvers = { } // Считаем заказы поставок, требующие действий - const pendingSupplyOrders = await prisma.supplyOrder.count({ + + // Наши расходники (созданные нами для себя) - требуют действий по статусам + const ourSupplyOrders = await prisma.supplyOrder.count({ where: { - OR: [ - // Заказы со статусом PENDING где мы - поставщик (нужно подтвердить) - { - status: "PENDING", - partnerId: currentUser.organization.id, - }, - // Заказы со статусом PENDING где мы - получатель ФФ (нужно подтвердить) - { - status: "PENDING", - fulfillmentCenterId: currentUser.organization.id, - }, - // Заказы со статусом IN_TRANSIT где мы - получатель ФФ (нужно подтвердить получение) - { - status: "IN_TRANSIT", - fulfillmentCenterId: currentUser.organization.id, - }, - ], + organizationId: currentUser.organization.id, // Создали мы + fulfillmentCenterId: currentUser.organization.id, // Получатель - мы + status: { in: ["CONFIRMED", "IN_TRANSIT"] }, // Подтверждено или в пути }, }); + // Расходники селлеров (созданные другими для нас) - требуют подтверждения получения + const sellerSupplyOrders = await prisma.supplyOrder.count({ + where: { + fulfillmentCenterId: currentUser.organization.id, // Получатель - мы + organizationId: { not: currentUser.organization.id }, // Создали НЕ мы + status: "IN_TRANSIT", // В пути - нужно подтвердить получение + }, + }); + + // Общий счетчик поставок + const pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders; + // Считаем входящие заявки на партнерство со статусом PENDING const pendingIncomingRequests = await prisma.counterpartyRequest.count({ where: { @@ -822,6 +822,8 @@ export const resolvers = { return { supplyOrders: pendingSupplyOrders, + ourSupplyOrders: ourSupplyOrders, // Наши расходники + sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров incomingRequests: pendingIncomingRequests, total: pendingSupplyOrders + pendingIncomingRequests, }; @@ -3284,12 +3286,16 @@ export const resolvers = { }, // Создать заказ поставки расходников - // Процесс: Селлер → Поставщик → Логистика → Фулфилмент - // 1. Селлер создает заказ у поставщика расходников + // Два сценария: + // 1. Селлер → Поставщик → Фулфилмент (селлер заказывает для фулфилмент-центра) + // 2. Фулфилмент → Поставщик → Фулфилмент (фулфилмент заказывает для себя) + // + // Процесс: Заказчик → Поставщик → [Логистика] → Фулфилмент + // 1. Заказчик (селлер или фулфилмент) создает заказ у поставщика расходников // 2. Поставщик получает заказ и готовит товары // 3. Логистика транспортирует товары на склад фулфилмента // 4. Фулфилмент принимает товары на склад - // 5. Все участники видят информацию о поставке в своих кабинетах + // 5. Расходники создаются в системе фулфилмент-центра createSupplyOrder: async ( _: unknown, args: { @@ -3488,6 +3494,7 @@ export const resolvers = { }); // Создаем расходники на основе заказанных товаров + // Расходники создаются в организации получателя (фулфилмент-центре) const suppliesData = args.input.items.map((item) => { const product = products.find((p) => p.id === item.productId)!; const productWithCategory = supplyOrder.items.find( @@ -3506,7 +3513,8 @@ export const resolvers = { supplier: partner.name || partner.fullName || "Не указан", minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток currentStock: 0, // Пока товар не пришел - organizationId: currentUser.organization!.id, + // Расходники создаются в организации получателя (фулфилмент-центре) + organizationId: fulfillmentCenterId || currentUser.organization!.id, }; }); diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index fd9708e..417e6ff 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -590,6 +590,8 @@ export const typeDefs = gql` type PendingSuppliesCount { supplyOrders: Int! + ourSupplyOrders: Int! # Наши расходники + sellerSupplyOrders: Int! # Расходники селлеров incomingRequests: Int! total: Int! }