From 80d33b46b8da873e71b8744b1cc85d774a892f30 Mon Sep 17 00:00:00 2001 From: Bivekich Date: Fri, 1 Aug 2025 12:39:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B5=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=20consumableType=20=D0=B2=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=20SupplyOrder=20=D0=B4=D0=BB=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=81=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=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=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20=D0=B8=20=D1=80=D0=B5=D0=B7=D0=BE=D0=BB?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=8B=20GraphQL=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=8F.=20?= =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BE=D1=81=D1=82=D0=B0=D1=82=D0=BA=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BE=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D0=B0=D1=81=D0=B0=D1=85=20=D0=BF=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=BB=D0=BE=D0=BD=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2.=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=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=D0=B8,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=20=D0=B4=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=83=D0=BF=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=80=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 1 + ...te-fulfillment-consumables-supply-page.tsx | 183 ++++++++++++++---- src/graphql/resolvers.ts | 35 ++++ src/graphql/typedefs.ts | 1 + 4 files changed, 178 insertions(+), 42 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9475f65..00b587e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -483,6 +483,7 @@ model SupplyOrder { totalItems Int fulfillmentCenterId String? logisticsPartnerId String + consumableType String? // Классификация расходников: FULFILLMENT_CONSUMABLES, SELLER_CONSUMABLES createdAt DateTime @default(now()) updatedAt DateTime @updatedAt organizationId String diff --git a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx index b54e307..c9bd5ad 100644 --- a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx +++ b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx @@ -29,6 +29,7 @@ import { GET_ALL_PRODUCTS, GET_SUPPLY_ORDERS, GET_MY_SUPPLIES, + GET_MY_FULFILLMENT_SUPPLIES, } from "@/graphql/queries"; import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations"; import { OrganizationAvatar } from "@/components/market/organization-avatar"; @@ -129,10 +130,12 @@ export function CreateFulfillmentConsumablesSupplyPage() { ); // Фильтруем товары по выбранному поставщику + // 📦 ТОЛЬКО РАСХОДНИКИ согласно правилам (раздел 2.1) const supplierProducts = selectedSupplier ? (productsData?.allProducts || []).filter( (product: FulfillmentConsumableProduct) => - product.organization.id === selectedSupplier.id + product.organization.id === selectedSupplier.id && + product.type === "CONSUMABLE" // Только расходники для фулфилмента ) : []; @@ -193,6 +196,16 @@ export function CreateFulfillmentConsumablesSupplyPage() { ); if (!product || !selectedSupplier) return; + // 🔒 ВАЛИДАЦИЯ ОСТАТКОВ согласно правилам (раздел 6.2) + if (quantity > 0) { + const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0); + + if (quantity > availableStock) { + toast.error(`❌ Недостаточно остатков!\nДоступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`); + return; + } + } + setSelectedConsumables((prev) => { const existing = prev.find((p) => p.id === productId); @@ -267,6 +280,8 @@ export function CreateFulfillmentConsumablesSupplyPage() { // Для фулфилмента указываем себя как получателя (поставка на свой склад) fulfillmentCenterId: user?.organization?.id, logisticsPartnerId: selectedLogistics?.id, + // 🏷️ КЛАССИФИКАЦИЯ согласно правилам (раздел 2.2) + consumableType: "FULFILLMENT_CONSUMABLES", // Расходники фулфилмента items: selectedConsumables.map((consumable) => ({ productId: consumable.id, quantity: consumable.selectedQuantity, @@ -276,6 +291,7 @@ export function CreateFulfillmentConsumablesSupplyPage() { refetchQueries: [ { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок { query: GET_MY_SUPPLIES }, // Обновляем расходники фулфилмента + { query: GET_MY_FULFILLMENT_SUPPLIES }, // 📊 Обновляем модуль учета расходников фулфилмента ], }); @@ -341,7 +357,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
-

+

Поставщики расходников

@@ -556,6 +572,23 @@ export function CreateFulfillmentConsumablesSupplyPage() {
{/* Изображение товара */}
+ {/* 🚫 ОВЕРЛЕЙ НЕДОСТУПНОСТИ */} + {(() => { + const totalStock = product.stock || product.quantity || 0; + const orderedStock = product.ordered || 0; + const availableStock = totalStock - orderedStock; + + if (availableStock <= 0) { + return ( +
+
+
НЕТ В НАЛИЧИИ
+
+
+ ); + } + return null; + })()} {product.images && product.images.length > 0 && product.images[0] ? ( @@ -595,40 +628,91 @@ export function CreateFulfillmentConsumablesSupplyPage() {

{product.name}

- {product.category && ( - - {product.category.name.slice(0, 10)} - - )} +
+ {product.category && ( + + {product.category.name.slice(0, 10)} + + )} + {/* 🚨 ИНДИКАТОР НИЗКИХ ОСТАТКОВ согласно правилам (раздел 6.3) */} + {(() => { + const totalStock = product.stock || product.quantity || 0; + const orderedStock = product.ordered || 0; + const availableStock = totalStock - orderedStock; + + if (availableStock <= 0) { + return ( + + Нет в наличии + + ); + } else if (availableStock <= 10) { + return ( + + Мало остатков + + ); + } + return null; + })()} +
{formatCurrency(product.price)} - {product.stock && ( - - {product.stock} - - )} + {/* 📊 АКТУАЛЬНЫЙ ОСТАТОК согласно правилам (раздел 6.4.2) */} +
+ {(() => { + const totalStock = product.stock || product.quantity || 0; + const orderedStock = product.ordered || 0; + const availableStock = totalStock - orderedStock; + + return ( +
+ + Доступно: {availableStock} + + {orderedStock > 0 && ( + + Заказано: {orderedStock} + + )} +
+ ); + })()} +
{/* Управление количеством */}
-
- + {(() => { + const totalStock = product.stock || product.quantity || 0; + const orderedStock = product.ordered || 0; + const availableStock = totalStock - orderedStock; + + return ( +
+ - -
+ +
+ ); + })()} {selectedQuantity > 0 && (
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 44ec6a1..c07ab38 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -3829,6 +3829,7 @@ export const resolvers = { logisticsPartnerId?: string; // ID логистической компании items: Array<{ productId: string; quantity: number }>; notes?: string; // Дополнительные заметки к заказу + consumableType?: string; // Классификация расходников }; }, context: Context @@ -3983,6 +3984,7 @@ export const resolvers = { organizationId: currentUser.organization.id, fulfillmentCenterId: fulfillmentCenterId, logisticsPartnerId: args.input.logisticsPartnerId, + consumableType: args.input.consumableType, // Классификация расходников status: initialStatus, items: { create: orderItems, @@ -4022,6 +4024,23 @@ export const resolvers = { }, }); + // 📦 РЕЗЕРВИРУЕМ ТОВАРЫ У ПОСТАВЩИКА + // Увеличиваем поле "ordered" для каждого заказанного товара + for (const item of args.input.items) { + await prisma.product.update({ + where: { id: item.productId }, + data: { + ordered: { + increment: item.quantity, + }, + }, + }); + } + + console.log(`📦 Зарезервированы товары для заказа ${supplyOrder.id}:`, + args.input.items.map(item => `${item.productId}: +${item.quantity} шт.`).join(', ')); + + // Создаем расходники на основе заказанных товаров // Расходники создаются в организации получателя (фулфилмент-центре) const suppliesData = args.input.items.map((item) => { @@ -6109,6 +6128,22 @@ export const resolvers = { }, }); + // 📦 СНИМАЕМ РЕЗЕРВАЦИЮ ПРИ ОТКЛОНЕНИИ + // Уменьшаем поле "ordered" для каждого отклоненного товара + for (const item of updatedOrder.items) { + await prisma.product.update({ + where: { id: item.productId }, + data: { + ordered: { + decrement: item.quantity, + }, + }, + }); + } + + console.log(`📦 Снята резервация при отклонении заказа ${updatedOrder.id}:`, + updatedOrder.items.map(item => `${item.productId}: -${item.quantity} шт.`).join(', ')); + return { success: true, message: args.reason diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 83c650c..f43452d 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -629,6 +629,7 @@ export const typeDefs = gql` logisticsPartnerId: ID! # ID логистической компании (обязательно) items: [SupplyOrderItemInput!]! notes: String # Дополнительные заметки к заказу + consumableType: String # Классификация расходников: FULFILLMENT_CONSUMABLES, SELLER_CONSUMABLES } input SupplyOrderItemInput {