Удален резервный файл employees-dashboard.tsx и добавлены новые функции для проверки уникальности артикула в форме продукта. Обновлены мутации GraphQL для поддержки проверки уникальности артикула, а также добавлены уведомления о низких остатках на складе. Оптимизирован интерфейс для улучшения пользовательского опыта.

This commit is contained in:
Bivekich
2025-08-01 12:10:48 +03:00
parent 52881cf302
commit 50b02f97b7
7 changed files with 566 additions and 804 deletions

View File

@ -876,6 +876,69 @@ export const DELETE_PRODUCT = gql`
}
`;
// Мутация для проверки уникальности артикула
export const CHECK_ARTICLE_UNIQUENESS = gql`
mutation CheckArticleUniqueness($article: String!, $excludeId: ID) {
checkArticleUniqueness(article: $article, excludeId: $excludeId) {
isUnique
existingProduct {
id
name
article
}
}
}
`;
// Мутация для резервирования товара (при заказе)
export const RESERVE_PRODUCT_STOCK = gql`
mutation ReserveProductStock($productId: ID!, $quantity: Int!) {
reserveProductStock(productId: $productId, quantity: $quantity) {
success
message
product {
id
quantity
ordered
stock
}
}
}
`;
// Мутация для освобождения резерва (при отмене заказа)
export const RELEASE_PRODUCT_RESERVE = gql`
mutation ReleaseProductReserve($productId: ID!, $quantity: Int!) {
releaseProductReserve(productId: $productId, quantity: $quantity) {
success
message
product {
id
quantity
ordered
stock
}
}
}
`;
// Мутация для обновления статуса "в пути"
export const UPDATE_PRODUCT_IN_TRANSIT = gql`
mutation UpdateProductInTransit($productId: ID!, $quantity: Int!, $operation: String!) {
updateProductInTransit(productId: $productId, quantity: $quantity, operation: $operation) {
success
message
product {
id
quantity
ordered
inTransit
stock
}
}
}
`;
// Мутации для корзины
export const ADD_TO_CART = gql`
mutation AddToCart($productId: ID!, $quantity: Int = 1) {

View File

@ -4393,6 +4393,227 @@ export const resolvers = {
}
},
// Проверка уникальности артикула
checkArticleUniqueness: async (
_: unknown,
args: { article: string; excludeId?: string },
context: Context
) => {
const { currentUser, prisma } = context;
if (!currentUser?.organization?.id) {
return {
isUnique: false,
existingProduct: null,
};
}
try {
const existingProduct = await prisma.product.findFirst({
where: {
article: args.article,
organizationId: currentUser.organization.id,
...(args.excludeId && { id: { not: args.excludeId } }),
},
select: {
id: true,
name: true,
article: true,
},
});
return {
isUnique: !existingProduct,
existingProduct,
};
} catch (error) {
console.error("Error checking article uniqueness:", error);
return {
isUnique: false,
existingProduct: null,
};
}
},
// Резервирование товара при создании заказа
reserveProductStock: async (
_: unknown,
args: { productId: string; quantity: number },
context: Context
) => {
const { currentUser, prisma } = context;
if (!currentUser?.organization?.id) {
return {
success: false,
message: "Необходимо авторизоваться",
};
}
try {
const product = await prisma.product.findUnique({
where: { id: args.productId },
});
if (!product) {
return {
success: false,
message: "Товар не найден",
};
}
// Проверяем доступность товара
const availableStock = (product.stock || product.quantity) - (product.ordered || 0);
if (availableStock < args.quantity) {
return {
success: false,
message: `Недостаточно товара на складе. Доступно: ${availableStock}, запрошено: ${args.quantity}`,
};
}
// Резервируем товар (увеличиваем поле ordered)
const updatedProduct = await prisma.product.update({
where: { id: args.productId },
data: {
ordered: (product.ordered || 0) + args.quantity,
},
});
console.log(`📦 Зарезервировано ${args.quantity} единиц товара ${product.name}`);
return {
success: true,
message: `Зарезервировано ${args.quantity} единиц товара`,
product: updatedProduct,
};
} catch (error) {
console.error("Error reserving product stock:", error);
return {
success: false,
message: "Ошибка при резервировании товара",
};
}
},
// Освобождение резерва при отмене заказа
releaseProductReserve: async (
_: unknown,
args: { productId: string; quantity: number },
context: Context
) => {
const { currentUser, prisma } = context;
if (!currentUser?.organization?.id) {
return {
success: false,
message: "Необходимо авторизоваться",
};
}
try {
const product = await prisma.product.findUnique({
where: { id: args.productId },
});
if (!product) {
return {
success: false,
message: "Товар не найден",
};
}
// Освобождаем резерв (уменьшаем поле ordered)
const newOrdered = Math.max((product.ordered || 0) - args.quantity, 0);
const updatedProduct = await prisma.product.update({
where: { id: args.productId },
data: {
ordered: newOrdered,
},
});
console.log(`🔄 Освобожден резерв ${args.quantity} единиц товара ${product.name}`);
return {
success: true,
message: `Освобожден резерв ${args.quantity} единиц товара`,
product: updatedProduct,
};
} catch (error) {
console.error("Error releasing product reserve:", error);
return {
success: false,
message: "Ошибка при освобождении резерва",
};
}
},
// Обновление статуса "в пути"
updateProductInTransit: async (
_: unknown,
args: { productId: string; quantity: number; operation: string },
context: Context
) => {
const { currentUser, prisma } = context;
if (!currentUser?.organization?.id) {
return {
success: false,
message: "Необходимо авторизоваться",
};
}
try {
const product = await prisma.product.findUnique({
where: { id: args.productId },
});
if (!product) {
return {
success: false,
message: "Товар не найден",
};
}
let newInTransit = product.inTransit || 0;
let newOrdered = product.ordered || 0;
if (args.operation === "ship") {
// При отгрузке: переводим из "заказано" в "в пути"
newInTransit = (product.inTransit || 0) + args.quantity;
newOrdered = Math.max((product.ordered || 0) - args.quantity, 0);
} else if (args.operation === "deliver") {
// При доставке: убираем из "в пути", добавляем в "продано"
newInTransit = Math.max((product.inTransit || 0) - args.quantity, 0);
}
const updatedProduct = await prisma.product.update({
where: { id: args.productId },
data: {
inTransit: newInTransit,
ordered: newOrdered,
...(args.operation === "deliver" && {
sold: (product.sold || 0) + args.quantity,
}),
},
});
console.log(`🚚 Обновлен статус "в пути" для товара ${product.name}: ${args.operation}`);
return {
success: true,
message: `Статус товара обновлен: ${args.operation}`,
product: updatedProduct,
};
} catch (error) {
console.error("Error updating product in transit:", error);
return {
success: false,
message: "Ошибка при обновлении статуса товара",
};
}
},
// Удалить товар
deleteProduct: async (
_: unknown,
@ -5608,6 +5829,24 @@ export const resolvers = {
})),
});
// 🔄 СИНХРОНИЗАЦИЯ: Обновляем товары поставщика (переводим из "в пути" в "продано")
for (const item of existingOrder.items) {
const product = await prisma.product.findUnique({
where: { id: item.product.id },
});
if (product) {
await prisma.product.update({
where: { id: item.product.id },
data: {
inTransit: Math.max((product.inTransit || 0) - item.quantity, 0),
sold: (product.sold || 0) + item.quantity,
},
});
console.log(`✅ Товар поставщика "${product.name}" обновлен: доставлено ${item.quantity} единиц`);
}
}
// Обновляем расходники
for (const item of existingOrder.items) {
console.log("📦 Обрабатываем товар:", {
@ -5735,6 +5974,48 @@ export const resolvers = {
}
console.log(`[DEBUG] Поставщик ${currentUser.organization.name} одобряет заказ ${args.id}`);
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Резервируем товары у поставщика
const orderWithItems = await prisma.supplyOrder.findUnique({
where: { id: args.id },
include: {
items: {
include: {
product: true,
},
},
},
});
if (orderWithItems) {
for (const item of orderWithItems.items) {
// Резервируем товар (увеличиваем поле ordered)
const product = await prisma.product.findUnique({
where: { id: item.product.id },
});
if (product) {
const availableStock = (product.stock || product.quantity) - (product.ordered || 0);
if (availableStock < item.quantity) {
return {
success: false,
message: `Недостаточно товара "${product.name}" на складе. Доступно: ${availableStock}, требуется: ${item.quantity}`,
};
}
await prisma.product.update({
where: { id: item.product.id },
data: {
ordered: (product.ordered || 0) + item.quantity,
},
});
console.log(`📦 Зарезервировано ${item.quantity} единиц товара "${product.name}"`);
}
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: "SUPPLIER_APPROVED" },
@ -5759,7 +6040,7 @@ export const resolvers = {
console.log(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`);
return {
success: true,
message: "Заказ поставки одобрен поставщиком",
message: "Заказ поставки одобрен поставщиком. Товары зарезервированы.",
order: updatedOrder,
};
} catch (error) {
@ -5880,6 +6161,38 @@ export const resolvers = {
};
}
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Переводим товары из "заказано" в "в пути"
const orderWithItems = await prisma.supplyOrder.findUnique({
where: { id: args.id },
include: {
items: {
include: {
product: true,
},
},
},
});
if (orderWithItems) {
for (const item of orderWithItems.items) {
const product = await prisma.product.findUnique({
where: { id: item.product.id },
});
if (product) {
await prisma.product.update({
where: { id: item.product.id },
data: {
ordered: Math.max((product.ordered || 0) - item.quantity, 0),
inTransit: (product.inTransit || 0) + item.quantity,
},
});
console.log(`🚚 Товар "${product.name}" переведен в статус "в пути": ${item.quantity} единиц`);
}
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: "SHIPPED" },
@ -5903,7 +6216,7 @@ export const resolvers = {
return {
success: true,
message: "Заказ отправлен поставщиком",
message: "Заказ отправлен поставщиком. Товары переведены в статус 'в пути'.",
order: updatedOrder,
};
} catch (error) {

View File

@ -228,6 +228,12 @@ export const typeDefs = gql`
createProduct(input: ProductInput!): ProductResponse!
updateProduct(id: ID!, input: ProductInput!): ProductResponse!
deleteProduct(id: ID!): Boolean!
# Валидация и управление остатками товаров
checkArticleUniqueness(article: String!, excludeId: ID): ArticleUniquenessResponse!
reserveProductStock(productId: ID!, quantity: Int!): ProductStockResponse!
releaseProductReserve(productId: ID!, quantity: Int!): ProductStockResponse!
updateProductInTransit(productId: ID!, quantity: Int!, operation: String!): ProductStockResponse!
# Работа с категориями
createCategory(input: CategoryInput!): CategoryResponse!
@ -750,6 +756,17 @@ export const typeDefs = gql`
product: Product
}
type ArticleUniquenessResponse {
isUnique: Boolean!
existingProduct: Product
}
type ProductStockResponse {
success: Boolean!
message: String!
product: Product
}
input CategoryInput {
name: String!
}