feat: Implement comprehensive three-party supply order workflow system
- Added logistics partner selection as mandatory requirement for fulfillment supply orders - Implemented complete status workflow: PENDING → SUPPLIER_APPROVED → LOGISTICS_CONFIRMED → SHIPPED → DELIVERED - Created dedicated interfaces for all three parties: * Fulfillment: Create orders with mandatory logistics selection and receive shipments * Suppliers: View, approve/reject orders, and ship approved orders via /supplies tab * Logistics: Confirm/reject transport requests via new /logistics-orders dashboard - Updated Prisma schema with logisticsPartnerId (non-nullable) and new SupplyOrderStatus enum - Added comprehensive GraphQL mutations for each party's workflow actions - Fixed GraphQL resolver to include logistics partners in supplyOrders query - Enhanced UI components with proper status badges and action buttons - Added backward compatibility for legacy status handling - Updated sidebar navigation routing for LOGIST organization type 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -899,13 +899,14 @@ export const resolvers = {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
// Возвращаем заказы где текущая организация является заказчиком, поставщиком или получателем
|
||||
// Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером
|
||||
return await prisma.supplyOrder.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
|
||||
{ partnerId: currentUser.organization.id }, // Заказы где организация - поставщик
|
||||
{ fulfillmentCenterId: currentUser.organization.id }, // Заказы где организация - получатель (фулфилмент)
|
||||
{ logisticsPartnerId: currentUser.organization.id }, // Заказы где организация - логистический партнер
|
||||
],
|
||||
},
|
||||
include: {
|
||||
@ -5253,11 +5254,15 @@ export const resolvers = {
|
||||
| "PENDING"
|
||||
| "CONFIRMED"
|
||||
| "IN_TRANSIT"
|
||||
| "SUPPLIER_APPROVED"
|
||||
| "LOGISTICS_CONFIRMED"
|
||||
| "SHIPPED"
|
||||
| "DELIVERED"
|
||||
| "CANCELLED";
|
||||
},
|
||||
context: Context
|
||||
) => {
|
||||
console.log(`[DEBUG] updateSupplyOrderStatus вызван для заказа ${args.id} со статусом ${args.status}`);
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
@ -5321,24 +5326,25 @@ export const resolvers = {
|
||||
},
|
||||
});
|
||||
|
||||
// Обновляем статусы расходников в зависимости от статуса заказа
|
||||
// ОТКЛЮЧЕНО: Устаревшая логика для обновления расходников
|
||||
// Теперь используются специальные мутации для каждой роли
|
||||
const targetOrganizationId = existingOrder.fulfillmentCenterId || existingOrder.organizationId;
|
||||
|
||||
if (args.status === "CONFIRMED") {
|
||||
// При подтверждении поставщиком - переводим расходники в статус "confirmed"
|
||||
await prisma.supply.updateMany({
|
||||
where: {
|
||||
organizationId: targetOrganizationId,
|
||||
status: "planned",
|
||||
// Находим расходники по названиям товаров из заказа
|
||||
name: {
|
||||
in: existingOrder.items.map(item => item.product.name)
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: "confirmed"
|
||||
}
|
||||
});
|
||||
console.log(`[WARNING] Попытка использовать устаревший статус CONFIRMED для заказа ${args.id}`);
|
||||
// Не обновляем расходники для устаревших статусов
|
||||
// await prisma.supply.updateMany({
|
||||
// where: {
|
||||
// organizationId: targetOrganizationId,
|
||||
// status: "planned",
|
||||
// name: {
|
||||
// in: existingOrder.items.map(item => item.product.name)
|
||||
// }
|
||||
// },
|
||||
// data: {
|
||||
// status: "confirmed"
|
||||
// }
|
||||
// });
|
||||
|
||||
console.log("✅ Статусы расходников обновлены на 'confirmed'");
|
||||
}
|
||||
@ -5463,6 +5469,485 @@ export const resolvers = {
|
||||
}
|
||||
},
|
||||
|
||||
// Резолверы для новых действий с заказами поставок
|
||||
supplierApproveOrder: async (
|
||||
_: unknown,
|
||||
args: { id: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем, что пользователь - поставщик этого заказа
|
||||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
partnerId: currentUser.organization.id, // Только поставщик может одобрить
|
||||
status: "PENDING", // Можно одобрить только заказы в статусе PENDING
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Заказ не найден или недоступен для одобрения",
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] Поставщик ${currentUser.organization.name} одобряет заказ ${args.id}`);
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: { status: "SUPPLIER_APPROVED" },
|
||||
include: {
|
||||
partner: true,
|
||||
organization: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`);
|
||||
return {
|
||||
success: true,
|
||||
message: "Заказ поставки одобрен поставщиком",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error approving supply order:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при одобрении заказа поставки",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
supplierRejectOrder: async (
|
||||
_: unknown,
|
||||
args: { id: string; reason?: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
try {
|
||||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
partnerId: currentUser.organization.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Заказ не найден или недоступен для отклонения",
|
||||
};
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: { status: "CANCELLED" },
|
||||
include: {
|
||||
partner: true,
|
||||
organization: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: args.reason
|
||||
? `Заказ отклонен поставщиком. Причина: ${args.reason}`
|
||||
: "Заказ отклонен поставщиком",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error rejecting supply order:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при отклонении заказа поставки",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
supplierShipOrder: async (
|
||||
_: unknown,
|
||||
args: { id: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
try {
|
||||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
partnerId: currentUser.organization.id,
|
||||
status: "LOGISTICS_CONFIRMED",
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Заказ не найден или недоступен для отправки",
|
||||
};
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: { status: "SHIPPED" },
|
||||
include: {
|
||||
partner: true,
|
||||
organization: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Заказ отправлен поставщиком",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error shipping supply order:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при отправке заказа поставки",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
logisticsConfirmOrder: async (
|
||||
_: unknown,
|
||||
args: { id: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
try {
|
||||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
logisticsPartnerId: currentUser.organization.id,
|
||||
status: "SUPPLIER_APPROVED",
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Заказ не найден или недоступен для подтверждения логистикой",
|
||||
};
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: { status: "LOGISTICS_CONFIRMED" },
|
||||
include: {
|
||||
partner: true,
|
||||
organization: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Заказ подтвержден логистической компанией",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error confirming supply order:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при подтверждении заказа логистикой",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
logisticsRejectOrder: async (
|
||||
_: unknown,
|
||||
args: { id: string; reason?: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
try {
|
||||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
logisticsPartnerId: currentUser.organization.id,
|
||||
status: "SUPPLIER_APPROVED",
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Заказ не найден или недоступен для отклонения логистикой",
|
||||
};
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: { status: "CANCELLED" },
|
||||
include: {
|
||||
partner: true,
|
||||
organization: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: args.reason
|
||||
? `Заказ отклонен логистической компанией. Причина: ${args.reason}`
|
||||
: "Заказ отклонен логистической компанией",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error rejecting supply order:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при отклонении заказа логистикой",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
fulfillmentReceiveOrder: async (
|
||||
_: unknown,
|
||||
args: { id: string },
|
||||
context: Context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
try {
|
||||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
fulfillmentCenterId: currentUser.organization.id,
|
||||
status: "SHIPPED",
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Заказ не найден или недоступен для приема",
|
||||
};
|
||||
}
|
||||
|
||||
// Обновляем статус заказа
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: { status: "DELIVERED" },
|
||||
include: {
|
||||
partner: true,
|
||||
organization: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Обновляем склад фулфилмента
|
||||
for (const item of existingOrder.items) {
|
||||
const existingSupply = await prisma.supply.findFirst({
|
||||
where: {
|
||||
organizationId: currentUser.organization.id,
|
||||
name: item.product.name,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSupply) {
|
||||
await prisma.supply.update({
|
||||
where: { id: existingSupply.id },
|
||||
data: {
|
||||
currentStock: existingSupply.currentStock + item.quantity,
|
||||
quantity: existingSupply.quantity + item.quantity,
|
||||
status: "in-stock",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.supply.create({
|
||||
data: {
|
||||
name: item.product.name,
|
||||
description: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
currentStock: item.quantity,
|
||||
usedStock: 0,
|
||||
unit: "шт",
|
||||
category: item.product.category?.name || "Расходники",
|
||||
status: "in-stock",
|
||||
supplier: updatedOrder.partner.name || updatedOrder.partner.fullName || "Поставщик",
|
||||
organizationId: currentUser.organization.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Заказ принят фулфилментом. Склад обновлен.",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error receiving supply order:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при приеме заказа поставки",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
updateExternalAdClicks: async (
|
||||
_: unknown,
|
||||
{ id, clicks }: { id: string; clicks: number },
|
||||
|
Reference in New Issue
Block a user