Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.
This commit is contained in:
22
src/graphql/context.ts
Normal file
22
src/graphql/context.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export interface Context {
|
||||
user: {
|
||||
id: string;
|
||||
organization?: {
|
||||
id: string;
|
||||
type: string;
|
||||
};
|
||||
} | null;
|
||||
currentUser?: {
|
||||
id: string;
|
||||
organization: {
|
||||
id: string;
|
||||
type: string;
|
||||
};
|
||||
} | null;
|
||||
admin?: {
|
||||
id: string;
|
||||
} | null;
|
||||
prisma: PrismaClient;
|
||||
}
|
@ -746,6 +746,42 @@ export const CREATE_SUPPLY_ORDER = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
// Мутация для назначения логистики на поставку фулфилментом
|
||||
export const ASSIGN_LOGISTICS_TO_SUPPLY = gql`
|
||||
mutation AssignLogisticsToSupply(
|
||||
$supplyOrderId: ID!
|
||||
$logisticsPartnerId: ID!
|
||||
$responsibleId: ID
|
||||
) {
|
||||
assignLogisticsToSupply(
|
||||
supplyOrderId: $supplyOrderId
|
||||
logisticsPartnerId: $logisticsPartnerId
|
||||
responsibleId: $responsibleId
|
||||
) {
|
||||
success
|
||||
message
|
||||
order {
|
||||
id
|
||||
status
|
||||
logisticsPartnerId
|
||||
responsibleId
|
||||
logisticsPartner {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
responsible {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Мутации для логистики
|
||||
export const CREATE_LOGISTICS = gql`
|
||||
mutation CreateLogistics($input: LogisticsInput!) {
|
||||
@ -924,8 +960,16 @@ export const RELEASE_PRODUCT_RESERVE = gql`
|
||||
|
||||
// Мутация для обновления статуса "в пути"
|
||||
export const UPDATE_PRODUCT_IN_TRANSIT = gql`
|
||||
mutation UpdateProductInTransit($productId: ID!, $quantity: Int!, $operation: String!) {
|
||||
updateProductInTransit(productId: $productId, quantity: $quantity, operation: $operation) {
|
||||
mutation UpdateProductInTransit(
|
||||
$productId: ID!
|
||||
$quantity: Int!
|
||||
$operation: String!
|
||||
) {
|
||||
updateProductInTransit(
|
||||
productId: $productId
|
||||
quantity: $quantity
|
||||
operation: $operation
|
||||
) {
|
||||
success
|
||||
message
|
||||
product {
|
||||
|
@ -111,6 +111,44 @@ export const GET_MY_FULFILLMENT_SUPPLIES = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_SELLER_SUPPLIES_ON_WAREHOUSE = gql`
|
||||
query GetSellerSuppliesOnWarehouse {
|
||||
sellerSuppliesOnWarehouse {
|
||||
id
|
||||
name
|
||||
description
|
||||
price
|
||||
quantity
|
||||
unit
|
||||
category
|
||||
status
|
||||
date
|
||||
supplier
|
||||
minStock
|
||||
currentStock
|
||||
usedStock
|
||||
imageUrl
|
||||
type
|
||||
shopLocation
|
||||
createdAt
|
||||
updatedAt
|
||||
organization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
sellerOwner {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
inn
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_MY_LOGISTICS = gql`
|
||||
query GetMyLogistics {
|
||||
myLogistics {
|
||||
@ -122,6 +160,25 @@ export const GET_MY_LOGISTICS = gql`
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
organization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_LOGISTICS_PARTNERS = gql`
|
||||
query GetLogisticsPartners {
|
||||
logisticsPartners {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
address
|
||||
phones
|
||||
emails
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -607,6 +664,8 @@ export const GET_MY_EMPLOYEES = gql`
|
||||
firstName
|
||||
lastName
|
||||
middleName
|
||||
fullName
|
||||
name
|
||||
birthDate
|
||||
avatar
|
||||
passportSeries
|
||||
@ -927,6 +986,7 @@ export const GET_SUPPLY_ORDERS = gql`
|
||||
supplyOrders {
|
||||
id
|
||||
organizationId
|
||||
partnerId
|
||||
deliveryDate
|
||||
status
|
||||
totalAmount
|
||||
@ -1023,11 +1083,7 @@ export const GET_SELLER_STATS_CACHE = gql`
|
||||
$dateFrom: String
|
||||
$dateTo: String
|
||||
) {
|
||||
getSellerStatsCache(
|
||||
period: $period
|
||||
dateFrom: $dateFrom
|
||||
dateTo: $dateTo
|
||||
) {
|
||||
getSellerStatsCache(period: $period, dateFrom: $dateFrom, dateTo: $dateTo) {
|
||||
success
|
||||
message
|
||||
fromCache
|
||||
|
@ -900,7 +900,7 @@ export const resolvers = {
|
||||
}
|
||||
|
||||
// Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером
|
||||
return await prisma.supplyOrder.findMany({
|
||||
const orders = await prisma.supplyOrder.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
|
||||
@ -939,6 +939,8 @@ export const resolvers = {
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return orders;
|
||||
},
|
||||
|
||||
// Счетчик поставок, требующих одобрения
|
||||
@ -969,12 +971,17 @@ export const resolvers = {
|
||||
},
|
||||
});
|
||||
|
||||
// Расходники селлеров (созданные другими для нас) - требуют подтверждения получения
|
||||
// Расходники селлеров (созданные другими для нас) - требуют действий фулфилмента
|
||||
const sellerSupplyOrders = await prisma.supplyOrder.count({
|
||||
where: {
|
||||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||||
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
|
||||
status: "IN_TRANSIT", // В пути - нужно подтвердить получение
|
||||
status: {
|
||||
in: [
|
||||
"SUPPLIER_APPROVED", // Поставщик подтвердил - нужно назначить логистику
|
||||
"IN_TRANSIT", // В пути - нужно подтвердить получение
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -986,9 +993,30 @@ export const resolvers = {
|
||||
},
|
||||
});
|
||||
|
||||
// Общий счетчик поставок
|
||||
const pendingSupplyOrders =
|
||||
ourSupplyOrders + sellerSupplyOrders + incomingSupplierOrders;
|
||||
// 🚚 ЛОГИСТИЧЕСКИЕ ЗАЯВКИ ДЛЯ ЛОГИСТИКИ (LOGIST) - требуют действий логистики
|
||||
const logisticsOrders = await prisma.supplyOrder.count({
|
||||
where: {
|
||||
logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика
|
||||
status: {
|
||||
in: [
|
||||
"CONFIRMED", // Подтверждено фулфилментом - нужно подтвердить логистикой
|
||||
"LOGISTICS_CONFIRMED", // Подтверждено логистикой - нужно забрать товар у поставщика
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Общий счетчик поставок в зависимости от типа организации
|
||||
let pendingSupplyOrders = 0;
|
||||
if (currentUser.organization.type === "FULFILLMENT") {
|
||||
pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders;
|
||||
} else if (currentUser.organization.type === "WHOLESALE") {
|
||||
pendingSupplyOrders = incomingSupplierOrders;
|
||||
} else if (currentUser.organization.type === "LOGIST") {
|
||||
pendingSupplyOrders = logisticsOrders;
|
||||
} else if (currentUser.organization.type === "SELLER") {
|
||||
pendingSupplyOrders = 0; // Селлеры не подтверждают поставки, только отслеживают
|
||||
}
|
||||
|
||||
// Считаем входящие заявки на партнерство со статусом PENDING
|
||||
const pendingIncomingRequests = await prisma.counterpartyRequest.count({
|
||||
@ -1003,6 +1031,7 @@ export const resolvers = {
|
||||
ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента
|
||||
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
|
||||
incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков
|
||||
logisticsOrders: logisticsOrders, // 🚚 Логистические заявки для логистики
|
||||
incomingRequests: pendingIncomingRequests,
|
||||
total: pendingSupplyOrders + pendingIncomingRequests,
|
||||
};
|
||||
@ -1146,9 +1175,11 @@ export const resolvers = {
|
||||
);
|
||||
|
||||
// Подсчитываем количество из таблицы Supply (актуальные остатки на складе фулфилмента)
|
||||
// ИСПРАВЛЕНО: считаем только расходники фулфилмента, исключаем расходники селлеров
|
||||
const fulfillmentSuppliesFromWarehouse = await prisma.supply.findMany({
|
||||
where: {
|
||||
organizationId: organizationId, // Склад фулфилмента
|
||||
type: "FULFILLMENT_CONSUMABLES", // ТОЛЬКО расходники фулфилмента
|
||||
},
|
||||
});
|
||||
|
||||
@ -1203,39 +1234,40 @@ export const resolvers = {
|
||||
`📊 FULFILLMENT SUPPLIES RECEIVED TODAY (ПРИБЫЛО): ${fulfillmentSuppliesReceivedToday.length} orders, ${fulfillmentSuppliesChangeToday} items`
|
||||
);
|
||||
|
||||
// Расходники селлеров - получаем из заказов от селлеров (расходники = CONSUMABLE)
|
||||
// Согласно правилам: селлеры заказывают расходники у поставщиков и доставляют на склад фулфилмента
|
||||
const sellerSuppliesCount = sellerDeliveredOrders.reduce(
|
||||
(sum, order) =>
|
||||
sum +
|
||||
order.items.reduce(
|
||||
(itemSum, item) =>
|
||||
itemSum +
|
||||
(item.product.type === "CONSUMABLE" ? item.quantity : 0),
|
||||
0
|
||||
),
|
||||
// Расходники селлеров - получаем из таблицы Supply (актуальные остатки на складе фулфилмента)
|
||||
// ИСПРАВЛЕНО: считаем из Supply с типом SELLER_CONSUMABLES
|
||||
const sellerSuppliesFromWarehouse = await prisma.supply.findMany({
|
||||
where: {
|
||||
organizationId: organizationId, // Склад фулфилмента
|
||||
type: "SELLER_CONSUMABLES", // ТОЛЬКО расходники селлеров
|
||||
},
|
||||
});
|
||||
|
||||
const sellerSuppliesCount = sellerSuppliesFromWarehouse.reduce(
|
||||
(sum, supply) => sum + (supply.currentStock || 0),
|
||||
0
|
||||
);
|
||||
|
||||
console.log(
|
||||
`💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from delivered orders)`
|
||||
`💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from Supply warehouse)`
|
||||
);
|
||||
|
||||
// Изменения расходников селлеров за сутки - используем уже полученные данные
|
||||
const sellerSuppliesChangeToday = recentSellerDeliveredOrders.reduce(
|
||||
(sum, order) =>
|
||||
sum +
|
||||
order.items.reduce(
|
||||
(itemSum, item) =>
|
||||
itemSum +
|
||||
(item.product.type === "CONSUMABLE" ? item.quantity : 0),
|
||||
0
|
||||
),
|
||||
// Изменения расходников селлеров за сутки - считаем из Supply записей, созданных за сутки
|
||||
const sellerSuppliesReceivedToday = await prisma.supply.findMany({
|
||||
where: {
|
||||
organizationId: organizationId, // Склад фулфилмента
|
||||
type: "SELLER_CONSUMABLES", // ТОЛЬКО расходники селлеров
|
||||
createdAt: { gte: oneDayAgo }, // Созданы за последние сутки
|
||||
},
|
||||
});
|
||||
|
||||
const sellerSuppliesChangeToday = sellerSuppliesReceivedToday.reduce(
|
||||
(sum, supply) => sum + (supply.currentStock || 0),
|
||||
0
|
||||
);
|
||||
|
||||
console.log(
|
||||
`📊 SELLER SUPPLIES RECEIVED TODAY: ${recentSellerDeliveredOrders.length} orders, ${sellerSuppliesChangeToday} items`
|
||||
`📊 SELLER SUPPLIES RECEIVED TODAY: ${sellerSuppliesReceivedToday.length} supplies, ${sellerSuppliesChangeToday} items`
|
||||
);
|
||||
|
||||
// Вычисляем процентные изменения
|
||||
@ -1327,6 +1359,24 @@ export const resolvers = {
|
||||
});
|
||||
},
|
||||
|
||||
// Логистические партнеры (организации-логисты)
|
||||
logisticsPartners: async (_: unknown, __: unknown, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем все организации типа LOGIST
|
||||
return await prisma.organization.findMany({
|
||||
where: {
|
||||
type: "LOGIST",
|
||||
// Убираем фильтр по статусу пока не определим правильные значения
|
||||
},
|
||||
orderBy: { createdAt: "desc" }, // Сортируем по дате создания вместо name
|
||||
});
|
||||
},
|
||||
|
||||
// Мои поставки Wildberries
|
||||
myWildberriesSupplies: async (
|
||||
_: unknown,
|
||||
@ -1358,6 +1408,94 @@ export const resolvers = {
|
||||
});
|
||||
},
|
||||
|
||||
// Расходники селлеров на складе фулфилмента (новый resolver)
|
||||
sellerSuppliesOnWarehouse: 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 sellerSupplies = await prisma.supply.findMany({
|
||||
where: {
|
||||
organizationId: currentUser.organization.id, // На складе этого фулфилмента
|
||||
type: "SELLER_CONSUMABLES" as const, // Только расходники селлеров
|
||||
sellerOwnerId: { not: null }, // ОБЯЗАТЕЛЬНО должен быть владелец-селлер
|
||||
},
|
||||
include: {
|
||||
organization: true, // Фулфилмент-центр (хранитель)
|
||||
sellerOwner: true, // Селлер-владелец расходников
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
// Логирование для отладки
|
||||
console.log(
|
||||
"🔍 ИСПРАВЛЕНО: Запрос расходников селлеров на складе фулфилмента:",
|
||||
{
|
||||
fulfillmentId: currentUser.organization.id,
|
||||
fulfillmentName: currentUser.organization.name,
|
||||
totalSupplies: sellerSupplies.length,
|
||||
sellerSupplies: sellerSupplies.map((supply) => ({
|
||||
id: supply.id,
|
||||
name: supply.name,
|
||||
type: supply.type,
|
||||
sellerOwnerId: supply.sellerOwnerId,
|
||||
sellerOwnerName:
|
||||
supply.sellerOwner?.name || supply.sellerOwner?.fullName,
|
||||
currentStock: supply.currentStock,
|
||||
})),
|
||||
}
|
||||
);
|
||||
|
||||
// ДВОЙНАЯ ПРОВЕРКА: Фильтруем на уровне кода для гарантии
|
||||
const filteredSupplies = sellerSupplies.filter((supply) => {
|
||||
const isValid =
|
||||
supply.type === "SELLER_CONSUMABLES" &&
|
||||
supply.sellerOwnerId != null &&
|
||||
supply.sellerOwner != null;
|
||||
|
||||
if (!isValid) {
|
||||
console.warn("⚠️ ОТФИЛЬТРОВАН некорректный расходник:", {
|
||||
id: supply.id,
|
||||
name: supply.name,
|
||||
type: supply.type,
|
||||
sellerOwnerId: supply.sellerOwnerId,
|
||||
hasSellerOwner: !!supply.sellerOwner,
|
||||
});
|
||||
}
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
console.log("✅ ФИНАЛЬНЫЙ РЕЗУЛЬТАТ после фильтрации:", {
|
||||
originalCount: sellerSupplies.length,
|
||||
filteredCount: filteredSupplies.length,
|
||||
removedCount: sellerSupplies.length - filteredSupplies.length,
|
||||
});
|
||||
|
||||
return filteredSupplies;
|
||||
},
|
||||
|
||||
// Мои товары и расходники (для поставщиков)
|
||||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||||
console.log("🔍 MY_PRODUCTS RESOLVER - ВЫЗВАН:", {
|
||||
@ -1830,34 +1968,52 @@ export const resolvers = {
|
||||
|
||||
// Сотрудники организации
|
||||
myEmployees: async (_: unknown, __: unknown, context: Context) => {
|
||||
console.log("🔍 myEmployees resolver called");
|
||||
|
||||
if (!context.user) {
|
||||
console.log("❌ No user in context for myEmployees");
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
console.log("✅ User authenticated for myEmployees:", context.user.id);
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
try {
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
console.log("❌ User has no organization");
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
console.log(
|
||||
"📊 User organization type:",
|
||||
currentUser.organization.type
|
||||
);
|
||||
|
||||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||||
console.log("❌ Not a fulfillment center");
|
||||
throw new GraphQLError("Доступно только для фулфилмент центров");
|
||||
}
|
||||
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { organizationId: currentUser.organization.id },
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
console.log("👥 Found employees:", employees.length);
|
||||
return employees;
|
||||
} catch (error) {
|
||||
console.error("❌ Error in myEmployees resolver:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||||
throw new GraphQLError("Доступно только для фулфилмент центров");
|
||||
}
|
||||
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { organizationId: currentUser.organization.id },
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return employees;
|
||||
},
|
||||
|
||||
// Получение сотрудника по ID
|
||||
@ -3937,7 +4093,19 @@ export const resolvers = {
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
console.log("🔍 Проверка пользователя:", {
|
||||
userId: context.user.id,
|
||||
userFound: !!currentUser,
|
||||
organizationFound: !!currentUser?.organization,
|
||||
organizationType: currentUser?.organization?.type,
|
||||
organizationId: currentUser?.organization?.id,
|
||||
});
|
||||
|
||||
if (!currentUser) {
|
||||
throw new GraphQLError("Пользователь не найден");
|
||||
}
|
||||
|
||||
if (!currentUser.organization) {
|
||||
throw new GraphQLError("У пользователя нет организации");
|
||||
}
|
||||
|
||||
@ -4067,21 +4235,34 @@ export const resolvers = {
|
||||
initialStatus = "CONFIRMED"; // Логист может сразу подтверждать заказы
|
||||
}
|
||||
|
||||
const supplyOrder = await prisma.supplyOrder.create({
|
||||
data: {
|
||||
partnerId: args.input.partnerId,
|
||||
deliveryDate: new Date(args.input.deliveryDate),
|
||||
totalAmount: new Prisma.Decimal(totalAmount),
|
||||
totalItems: totalItems,
|
||||
organizationId: currentUser.organization.id,
|
||||
fulfillmentCenterId: fulfillmentCenterId,
|
||||
logisticsPartnerId: args.input.logisticsPartnerId,
|
||||
consumableType: args.input.consumableType, // Классификация расходников
|
||||
status: initialStatus,
|
||||
items: {
|
||||
create: orderItems,
|
||||
},
|
||||
// Подготавливаем данные для создания заказа
|
||||
const createData: any = {
|
||||
partnerId: args.input.partnerId,
|
||||
deliveryDate: new Date(args.input.deliveryDate),
|
||||
totalAmount: new Prisma.Decimal(totalAmount),
|
||||
totalItems: totalItems,
|
||||
organizationId: currentUser.organization.id,
|
||||
fulfillmentCenterId: fulfillmentCenterId,
|
||||
consumableType: args.input.consumableType,
|
||||
status: initialStatus,
|
||||
items: {
|
||||
create: orderItems,
|
||||
},
|
||||
};
|
||||
|
||||
// 🔄 ЛОГИСТИКА ОПЦИОНАЛЬНА: добавляем только если передана
|
||||
if (args.input.logisticsPartnerId) {
|
||||
createData.logisticsPartnerId = args.input.logisticsPartnerId;
|
||||
}
|
||||
|
||||
console.log("🔍 Создаем SupplyOrder с данными:", {
|
||||
hasLogistics: !!args.input.logisticsPartnerId,
|
||||
logisticsId: args.input.logisticsPartnerId,
|
||||
createData: createData,
|
||||
});
|
||||
|
||||
const supplyOrder = await prisma.supplyOrder.create({
|
||||
data: createData,
|
||||
include: {
|
||||
partner: {
|
||||
include: {
|
||||
@ -5961,17 +6142,13 @@ export const resolvers = {
|
||||
});
|
||||
|
||||
if (product) {
|
||||
// Согласно правилам: Основные значения = Предыдущие остатки + Прибыло - Убыло
|
||||
const currentStock = product.stock || product.quantity || 0;
|
||||
const newStock = Math.max(currentStock - item.quantity, 0);
|
||||
|
||||
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
|
||||
// Остаток уже был уменьшен при создании/одобрении заказа
|
||||
await prisma.product.update({
|
||||
where: { id: item.product.id },
|
||||
data: {
|
||||
// Обновляем основные остатки (УБЫЛО)
|
||||
stock: newStock,
|
||||
quantity: newStock, // Синхронизируем оба поля для совместимости
|
||||
// Обновляем дополнительные значения
|
||||
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
|
||||
// Только переводим из inTransit в sold
|
||||
inTransit: Math.max(
|
||||
(product.inTransit || 0) - item.quantity,
|
||||
0
|
||||
@ -5980,7 +6157,11 @@ export const resolvers = {
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`✅ Товар поставщика "${product.name}" обновлен: доставлено ${item.quantity} единиц (остаток: ${currentStock} -> ${newStock})`
|
||||
`✅ Товар поставщика "${product.name}" обновлен: доставлено ${
|
||||
item.quantity
|
||||
} единиц (остаток НЕ ИЗМЕНЕН: ${
|
||||
product.stock || product.quantity || 0
|
||||
})`
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6073,6 +6254,117 @@ export const resolvers = {
|
||||
}
|
||||
},
|
||||
|
||||
// Назначение логистики фулфилментом на заказ селлера
|
||||
assignLogisticsToSupply: async (
|
||||
_: unknown,
|
||||
args: {
|
||||
supplyOrderId: string;
|
||||
logisticsPartnerId: string;
|
||||
responsibleId?: 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("У пользователя нет организации");
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь - фулфилмент
|
||||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||||
throw new GraphQLError("Только фулфилмент может назначать логистику");
|
||||
}
|
||||
|
||||
try {
|
||||
// Находим заказ
|
||||
const existingOrder = await prisma.supplyOrder.findUnique({
|
||||
where: { id: args.supplyOrderId },
|
||||
include: {
|
||||
partner: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: { product: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
throw new GraphQLError("Заказ поставки не найден");
|
||||
}
|
||||
|
||||
// Проверяем, что это заказ для нашего фулфилмент-центра
|
||||
if (existingOrder.fulfillmentCenterId !== currentUser.organization.id) {
|
||||
throw new GraphQLError("Нет доступа к этому заказу");
|
||||
}
|
||||
|
||||
// Проверяем, что статус позволяет назначить логистику
|
||||
if (existingOrder.status !== "SUPPLIER_APPROVED") {
|
||||
throw new GraphQLError(
|
||||
`Нельзя назначить логистику для заказа со статусом ${existingOrder.status}`
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем, что логистическая компания существует
|
||||
const logisticsPartner = await prisma.organization.findUnique({
|
||||
where: { id: args.logisticsPartnerId },
|
||||
});
|
||||
|
||||
if (!logisticsPartner || logisticsPartner.type !== "LOGIST") {
|
||||
throw new GraphQLError("Логистическая компания не найдена");
|
||||
}
|
||||
|
||||
// Обновляем заказ
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.supplyOrderId },
|
||||
data: {
|
||||
logisticsPartner: {
|
||||
connect: { id: args.logisticsPartnerId },
|
||||
},
|
||||
status: "CONFIRMED", // Переводим в статус "подтвержден фулфилментом"
|
||||
},
|
||||
include: {
|
||||
partner: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: { product: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Логистика назначена на заказ ${args.supplyOrderId}:`, {
|
||||
logisticsPartner: logisticsPartner.name,
|
||||
responsible: args.responsibleId,
|
||||
newStatus: "CONFIRMED",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Логистика успешно назначена",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Ошибка при назначении логистики:", error);
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Ошибка при назначении логистики",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Резолверы для новых действий с заказами поставок
|
||||
supplierApproveOrder: async (
|
||||
_: unknown,
|
||||
@ -6458,7 +6750,7 @@ export const resolvers = {
|
||||
where: {
|
||||
id: args.id,
|
||||
logisticsPartnerId: currentUser.organization.id,
|
||||
status: "SUPPLIER_APPROVED",
|
||||
OR: [{ status: "SUPPLIER_APPROVED" }, { status: "CONFIRMED" }],
|
||||
},
|
||||
});
|
||||
|
||||
@ -6530,7 +6822,7 @@ export const resolvers = {
|
||||
where: {
|
||||
id: args.id,
|
||||
logisticsPartnerId: currentUser.organization.id,
|
||||
status: "SUPPLIER_APPROVED",
|
||||
OR: [{ status: "SUPPLIER_APPROVED" }, { status: "CONFIRMED" }],
|
||||
},
|
||||
});
|
||||
|
||||
@ -6608,9 +6900,15 @@ export const resolvers = {
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
organization: true, // Селлер-создатель заказа
|
||||
partner: true, // Поставщик
|
||||
},
|
||||
});
|
||||
|
||||
@ -6651,17 +6949,13 @@ export const resolvers = {
|
||||
});
|
||||
|
||||
if (product) {
|
||||
// Согласно правилам: Основные значения = Предыдущие остатки + Прибыло - Убыло
|
||||
const currentStock = product.stock || product.quantity || 0;
|
||||
const newStock = Math.max(currentStock - item.quantity, 0);
|
||||
|
||||
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
|
||||
// Остаток уже был уменьшен при создании/одобрении заказа
|
||||
await prisma.product.update({
|
||||
where: { id: item.product.id },
|
||||
data: {
|
||||
// Обновляем основные остатки (УБЫЛО)
|
||||
stock: newStock,
|
||||
quantity: newStock, // Синхронизируем оба поля для совместимости
|
||||
// Обновляем дополнительные значения
|
||||
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
|
||||
// Только переводим из inTransit в sold
|
||||
inTransit: Math.max(
|
||||
(product.inTransit || 0) - item.quantity,
|
||||
0
|
||||
@ -6670,33 +6964,62 @@ export const resolvers = {
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`✅ Товар поставщика "${product.name}" обновлен: доставлено ${item.quantity} единиц`
|
||||
`✅ Товар поставщика "${product.name}" обновлен: получено ${item.quantity} единиц`
|
||||
);
|
||||
console.log(
|
||||
` 📊 Остатки: ${currentStock} -> ${newStock} (УБЫЛО: ${item.quantity})`
|
||||
` 📊 Остаток: ${
|
||||
product.stock || product.quantity || 0
|
||||
} (НЕ ИЗМЕНЕН - уже списан при заказе)`
|
||||
);
|
||||
console.log(
|
||||
` 🚚 В пути: ${product.inTransit || 0} -> ${Math.max(
|
||||
(product.inTransit || 0) - item.quantity,
|
||||
0
|
||||
)}`
|
||||
)} (УБЫЛО: ${item.quantity})`
|
||||
);
|
||||
console.log(
|
||||
` 💰 Продано: ${product.sold || 0} -> ${
|
||||
(product.sold || 0) + item.quantity
|
||||
}`
|
||||
} (ПРИБЫЛО: ${item.quantity})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем склад фулфилмента
|
||||
// Обновляем склад фулфилмента с учетом типа расходников
|
||||
console.log("📦 Обновляем склад фулфилмента...");
|
||||
console.log(
|
||||
`🏷️ Тип поставки: ${
|
||||
existingOrder.consumableType || "FULFILLMENT_CONSUMABLES"
|
||||
}`
|
||||
);
|
||||
|
||||
for (const item of existingOrder.items) {
|
||||
// Определяем тип расходников и владельца
|
||||
const isSellerSupply =
|
||||
existingOrder.consumableType === "SELLER_CONSUMABLES";
|
||||
const supplyType = isSellerSupply
|
||||
? "SELLER_CONSUMABLES"
|
||||
: "FULFILLMENT_CONSUMABLES";
|
||||
const sellerOwnerId = isSellerSupply
|
||||
? updatedOrder.organization?.id
|
||||
: null;
|
||||
|
||||
// Для расходников селлеров ищем по имени И по владельцу
|
||||
const whereCondition = isSellerSupply
|
||||
? {
|
||||
organizationId: currentUser.organization.id,
|
||||
name: item.product.name,
|
||||
type: "SELLER_CONSUMABLES" as const,
|
||||
sellerOwnerId: sellerOwnerId,
|
||||
}
|
||||
: {
|
||||
organizationId: currentUser.organization.id,
|
||||
name: item.product.name,
|
||||
type: "FULFILLMENT_CONSUMABLES" as const,
|
||||
};
|
||||
|
||||
const existingSupply = await prisma.supply.findFirst({
|
||||
where: {
|
||||
organizationId: currentUser.organization.id,
|
||||
name: item.product.name,
|
||||
},
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
if (existingSupply) {
|
||||
@ -6709,9 +7032,13 @@ export const resolvers = {
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`📈 Обновлен существующий расходник фулфилмента "${
|
||||
item.product.name
|
||||
}": ${existingSupply.currentStock} -> ${
|
||||
`📈 Обновлен существующий ${
|
||||
isSellerSupply ? "расходник селлера" : "расходник фулфилмента"
|
||||
} "${item.product.name}" ${
|
||||
isSellerSupply
|
||||
? `(владелец: ${updatedOrder.organization?.name})`
|
||||
: ""
|
||||
}: ${existingSupply.currentStock} -> ${
|
||||
existingSupply.currentStock + item.quantity
|
||||
}`
|
||||
);
|
||||
@ -6719,9 +7046,13 @@ export const resolvers = {
|
||||
await prisma.supply.create({
|
||||
data: {
|
||||
name: item.product.name,
|
||||
description:
|
||||
item.product.description ||
|
||||
`Расходники от ${updatedOrder.partner.name}`,
|
||||
description: isSellerSupply
|
||||
? `Расходники селлера ${
|
||||
updatedOrder.organization?.name ||
|
||||
updatedOrder.organization?.fullName
|
||||
}`
|
||||
: item.product.description ||
|
||||
`Расходники от ${updatedOrder.partner.name}`,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
currentStock: item.quantity,
|
||||
@ -6733,11 +7064,21 @@ export const resolvers = {
|
||||
updatedOrder.partner.name ||
|
||||
updatedOrder.partner.fullName ||
|
||||
"Поставщик",
|
||||
type: supplyType as
|
||||
| "SELLER_CONSUMABLES"
|
||||
| "FULFILLMENT_CONSUMABLES",
|
||||
sellerOwnerId: sellerOwnerId,
|
||||
organizationId: currentUser.organization.id,
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`➕ Создан новый расходник фулфилмента "${item.product.name}": ${item.quantity} единиц`
|
||||
`➕ Создан новый ${
|
||||
isSellerSupply ? "расходник селлера" : "расходник фулфилмента"
|
||||
} "${item.product.name}" ${
|
||||
isSellerSupply
|
||||
? `(владелец: ${updatedOrder.organization?.name})`
|
||||
: ""
|
||||
}: ${item.quantity} единиц`
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6849,7 +7190,10 @@ export const resolvers = {
|
||||
// Иначе загружаем отдельно
|
||||
return await prisma.supply.findMany({
|
||||
where: { organizationId: parent.id },
|
||||
include: { organization: true },
|
||||
include: {
|
||||
organization: true,
|
||||
sellerOwner: true, // Включаем информацию о селлере-владельце
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
},
|
||||
@ -6949,6 +7293,20 @@ export const resolvers = {
|
||||
},
|
||||
|
||||
Employee: {
|
||||
fullName: (parent: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleName?: string;
|
||||
}) => {
|
||||
const parts = [parent.lastName, parent.firstName];
|
||||
if (parent.middleName) {
|
||||
parts.push(parent.middleName);
|
||||
}
|
||||
return parts.join(" ");
|
||||
},
|
||||
name: (parent: { firstName: string; lastName: string }) => {
|
||||
return `${parent.firstName} ${parent.lastName}`;
|
||||
},
|
||||
birthDate: (parent: { birthDate?: Date | string | null }) => {
|
||||
if (!parent.birthDate) return null;
|
||||
if (parent.birthDate instanceof Date) {
|
||||
|
6
src/graphql/resolvers/auth.ts
Normal file
6
src/graphql/resolvers/auth.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Context } from "../context";
|
||||
|
||||
export const authResolvers = {
|
||||
Query: {},
|
||||
Mutation: {},
|
||||
};
|
6
src/graphql/resolvers/employees.ts
Normal file
6
src/graphql/resolvers/employees.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Context } from "../context";
|
||||
|
||||
export const employeeResolvers = {
|
||||
Query: {},
|
||||
Mutation: {},
|
||||
};
|
84
src/graphql/resolvers/index.ts
Normal file
84
src/graphql/resolvers/index.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { JSONScalar, DateTimeScalar } from "../scalars";
|
||||
import { authResolvers } from "./auth";
|
||||
import { employeeResolvers } from "./employees";
|
||||
import { logisticsResolvers } from "./logistics";
|
||||
import { suppliesResolvers } from "./supplies";
|
||||
|
||||
// Функция для объединения резолверов
|
||||
const mergeResolvers = (...resolvers: any[]) => {
|
||||
const result: any = {
|
||||
Query: {},
|
||||
Mutation: {},
|
||||
};
|
||||
|
||||
for (const resolver of resolvers) {
|
||||
if (resolver.Query) {
|
||||
Object.assign(result.Query, resolver.Query);
|
||||
}
|
||||
if (resolver.Mutation) {
|
||||
Object.assign(result.Mutation, resolver.Mutation);
|
||||
}
|
||||
// Объединяем другие типы резолверов (например, Employee, Organization и т.д.)
|
||||
for (const [key, value] of Object.entries(resolver)) {
|
||||
if (key !== "Query" && key !== "Mutation") {
|
||||
if (!result[key]) {
|
||||
result[key] = {};
|
||||
}
|
||||
Object.assign(result[key], value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Временно импортируем старые резолверы для частей, которые еще не вынесены
|
||||
// TODO: Постепенно убрать это после полного рефакторинга
|
||||
import { resolvers as oldResolvers } from "../resolvers";
|
||||
|
||||
// Объединяем новые модульные резолверы с остальными старыми
|
||||
export const resolvers = mergeResolvers(
|
||||
// Скалярные типы
|
||||
{
|
||||
JSON: JSONScalar,
|
||||
DateTime: DateTimeScalar,
|
||||
},
|
||||
|
||||
// Новые модульные резолверы
|
||||
authResolvers,
|
||||
employeeResolvers,
|
||||
logisticsResolvers,
|
||||
suppliesResolvers,
|
||||
|
||||
// Временно добавляем старые резолверы, исключая уже вынесенные
|
||||
{
|
||||
Query: {
|
||||
...oldResolvers.Query,
|
||||
// Исключаем уже вынесенные Query
|
||||
myEmployees: undefined,
|
||||
logisticsPartners: undefined,
|
||||
pendingSuppliesCount: undefined,
|
||||
},
|
||||
Mutation: {
|
||||
...oldResolvers.Mutation,
|
||||
// Исключаем уже вынесенные Mutation
|
||||
sendSmsCode: undefined,
|
||||
verifySmsCode: undefined,
|
||||
verifyInn: undefined,
|
||||
registerFulfillmentOrganization: undefined,
|
||||
createEmployee: undefined,
|
||||
updateEmployee: undefined,
|
||||
deleteEmployee: undefined,
|
||||
assignLogisticsToSupply: undefined,
|
||||
logisticsConfirmOrder: undefined,
|
||||
logisticsRejectOrder: undefined,
|
||||
},
|
||||
// Остальные типы пока оставляем из старых резолверов
|
||||
User: oldResolvers.User,
|
||||
Organization: oldResolvers.Organization,
|
||||
Product: oldResolvers.Product,
|
||||
// SupplyOrder: oldResolvers.SupplyOrder, // Удалено: отсутствует в старых резолверах
|
||||
// Employee берем из нового модуля
|
||||
Employee: undefined,
|
||||
}
|
||||
);
|
285
src/graphql/resolvers/logistics.ts
Normal file
285
src/graphql/resolvers/logistics.ts
Normal file
@ -0,0 +1,285 @@
|
||||
import { GraphQLError } from "graphql";
|
||||
import { Context } from "../context";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
|
||||
export const logisticsResolvers = {
|
||||
Query: {
|
||||
// Получить логистические компании-партнеры
|
||||
logisticsPartners: async (_: unknown, __: unknown, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError("Требуется авторизация", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем все организации типа LOGIST
|
||||
return await prisma.organization.findMany({
|
||||
where: {
|
||||
type: "LOGIST",
|
||||
// Убираем фильтр по статусу пока не определим правильные значения
|
||||
},
|
||||
orderBy: { createdAt: "desc" }, // Сортируем по дате создания вместо name
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
// Назначить логистику на поставку (используется фулфилментом)
|
||||
assignLogisticsToSupply: async (
|
||||
_: unknown,
|
||||
args: {
|
||||
supplyOrderId: string;
|
||||
logisticsPartnerId: string;
|
||||
responsibleId?: 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("У пользователя нет организации");
|
||||
}
|
||||
|
||||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||||
throw new GraphQLError("Доступно только для фулфилмент центров");
|
||||
}
|
||||
|
||||
try {
|
||||
// Находим заказ
|
||||
const existingOrder = await prisma.supplyOrder.findUnique({
|
||||
where: { id: args.supplyOrderId },
|
||||
include: {
|
||||
partner: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: { product: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
throw new GraphQLError("Заказ поставки не найден");
|
||||
}
|
||||
|
||||
// Проверяем, что это заказ для нашего фулфилмент-центра
|
||||
if (existingOrder.fulfillmentCenterId !== currentUser.organization.id) {
|
||||
throw new GraphQLError("Нет доступа к этому заказу");
|
||||
}
|
||||
|
||||
// Проверяем, что статус позволяет назначить логистику
|
||||
if (existingOrder.status !== "SUPPLIER_APPROVED") {
|
||||
throw new GraphQLError(
|
||||
`Нельзя назначить логистику для заказа со статусом ${existingOrder.status}`
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем, что логистическая компания существует
|
||||
const logisticsPartner = await prisma.organization.findUnique({
|
||||
where: { id: args.logisticsPartnerId },
|
||||
});
|
||||
|
||||
if (!logisticsPartner || logisticsPartner.type !== "LOGIST") {
|
||||
throw new GraphQLError("Логистическая компания не найдена");
|
||||
}
|
||||
|
||||
// Обновляем заказ
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.supplyOrderId },
|
||||
data: {
|
||||
logisticsPartner: {
|
||||
connect: { id: args.logisticsPartnerId },
|
||||
},
|
||||
status: "CONFIRMED", // Переводим в статус "подтвержден фулфилментом"
|
||||
},
|
||||
include: {
|
||||
partner: true,
|
||||
fulfillmentCenter: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: { product: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Логистика назначена на заказ ${args.supplyOrderId}:`, {
|
||||
logisticsPartner: logisticsPartner.name,
|
||||
responsible: args.responsibleId,
|
||||
newStatus: "CONFIRMED",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Логистика успешно назначена",
|
||||
order: updatedOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Ошибка при назначении логистики:", error);
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.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,
|
||||
OR: [{ status: "SUPPLIER_APPROVED" }, { status: "CONFIRMED" }],
|
||||
},
|
||||
});
|
||||
|
||||
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 },
|
||||
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,
|
||||
OR: [{ status: "SUPPLIER_APPROVED" }, { status: "CONFIRMED" }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingOrder) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Заказ не найден или недоступен для отклонения логистикой",
|
||||
};
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.supplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: {
|
||||
status: "CANCELLED",
|
||||
logisticsPartnerId: null, // Убираем назначенную логистику
|
||||
},
|
||||
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 rejecting supply order:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Ошибка при отклонении заказа",
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
6
src/graphql/resolvers/supplies.ts
Normal file
6
src/graphql/resolvers/supplies.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Context } from "../context";
|
||||
|
||||
export const suppliesResolvers = {
|
||||
Query: {},
|
||||
Mutation: {},
|
||||
};
|
51
src/graphql/scalars.ts
Normal file
51
src/graphql/scalars.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { GraphQLScalarType, Kind } from "graphql";
|
||||
|
||||
export const JSONScalar = new GraphQLScalarType({
|
||||
name: "JSON",
|
||||
serialize: (value) => value,
|
||||
parseValue: (value) => value,
|
||||
parseLiteral: (ast) => {
|
||||
switch (ast.kind) {
|
||||
case Kind.STRING:
|
||||
case Kind.BOOLEAN:
|
||||
return ast.value;
|
||||
case Kind.INT:
|
||||
case Kind.FLOAT:
|
||||
return parseFloat(ast.value);
|
||||
case Kind.OBJECT:
|
||||
return ast.fields.reduce(
|
||||
(accumulator, field) => ({
|
||||
...accumulator,
|
||||
[field.name.value]: JSONScalar.parseLiteral(field.value),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
case Kind.LIST:
|
||||
return ast.values.map((n) => JSONScalar.parseLiteral(n));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const DateTimeScalar = new GraphQLScalarType({
|
||||
name: "DateTime",
|
||||
serialize: (value) => {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
parseValue: (value) => {
|
||||
if (typeof value === "string") {
|
||||
return new Date(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
parseLiteral: (ast) => {
|
||||
if (ast.kind === Kind.STRING) {
|
||||
return new Date(ast.value);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
@ -43,6 +43,9 @@ export const typeDefs = gql`
|
||||
# Расходники фулфилмента (материалы для работы фулфилмента)
|
||||
myFulfillmentSupplies: [Supply!]!
|
||||
|
||||
# Расходники селлеров на складе фулфилмента (только для фулфилмента)
|
||||
sellerSuppliesOnWarehouse: [Supply!]!
|
||||
|
||||
# Заказы поставок расходников
|
||||
supplyOrders: [SupplyOrder!]!
|
||||
|
||||
@ -52,6 +55,9 @@ export const typeDefs = gql`
|
||||
# Логистика организации
|
||||
myLogistics: [Logistics!]!
|
||||
|
||||
# Логистические партнеры (организации-логисты)
|
||||
logisticsPartners: [Organization!]!
|
||||
|
||||
# Поставки Wildberries
|
||||
myWildberriesSupplies: [WildberriesSupply!]!
|
||||
|
||||
@ -206,16 +212,23 @@ export const typeDefs = gql`
|
||||
id: ID!
|
||||
status: SupplyOrderStatus!
|
||||
): SupplyOrderResponse!
|
||||
|
||||
|
||||
# Назначение логистики фулфилментом
|
||||
assignLogisticsToSupply(
|
||||
supplyOrderId: ID!
|
||||
logisticsPartnerId: ID!
|
||||
responsibleId: ID
|
||||
): SupplyOrderResponse!
|
||||
|
||||
# Действия поставщика
|
||||
supplierApproveOrder(id: ID!): SupplyOrderResponse!
|
||||
supplierRejectOrder(id: ID!, reason: String): SupplyOrderResponse!
|
||||
supplierShipOrder(id: ID!): SupplyOrderResponse!
|
||||
|
||||
|
||||
# Действия логиста
|
||||
logisticsConfirmOrder(id: ID!): SupplyOrderResponse!
|
||||
logisticsRejectOrder(id: ID!, reason: String): SupplyOrderResponse!
|
||||
|
||||
|
||||
# Действия фулфилмента
|
||||
fulfillmentReceiveOrder(id: ID!): SupplyOrderResponse!
|
||||
|
||||
@ -228,12 +241,19 @@ export const typeDefs = gql`
|
||||
createProduct(input: ProductInput!): ProductResponse!
|
||||
updateProduct(id: ID!, input: ProductInput!): ProductResponse!
|
||||
deleteProduct(id: ID!): Boolean!
|
||||
|
||||
|
||||
# Валидация и управление остатками товаров
|
||||
checkArticleUniqueness(article: String!, excludeId: ID): ArticleUniquenessResponse!
|
||||
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!
|
||||
updateProductInTransit(
|
||||
productId: ID!
|
||||
quantity: Int!
|
||||
operation: String!
|
||||
): ProductStockResponse!
|
||||
|
||||
# Работа с категориями
|
||||
createCategory(input: CategoryInput!): CategoryResponse!
|
||||
@ -535,6 +555,11 @@ export const typeDefs = gql`
|
||||
}
|
||||
|
||||
# Типы для расходников
|
||||
enum SupplyType {
|
||||
FULFILLMENT_CONSUMABLES # Расходники фулфилмента (купленные фулфилментом для себя)
|
||||
SELLER_CONSUMABLES # Расходники селлеров (принятые от селлеров для хранения)
|
||||
}
|
||||
|
||||
type Supply {
|
||||
id: ID!
|
||||
name: String!
|
||||
@ -550,6 +575,9 @@ export const typeDefs = gql`
|
||||
currentStock: Int
|
||||
usedStock: Int
|
||||
imageUrl: String
|
||||
type: SupplyType!
|
||||
sellerOwner: Organization # Селлер-владелец (для расходников селлеров)
|
||||
shopLocation: String # Местоположение в магазине фулфилмента
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
organization: Organization!
|
||||
@ -594,8 +622,8 @@ export const typeDefs = gql`
|
||||
totalItems: Int!
|
||||
fulfillmentCenterId: ID
|
||||
fulfillmentCenter: Organization
|
||||
logisticsPartnerId: ID!
|
||||
logisticsPartner: Organization!
|
||||
logisticsPartnerId: ID
|
||||
logisticsPartner: Organization
|
||||
items: [SupplyOrderItem!]!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
@ -612,21 +640,21 @@ export const typeDefs = gql`
|
||||
}
|
||||
|
||||
enum SupplyOrderStatus {
|
||||
PENDING # Ожидает одобрения поставщика
|
||||
CONFIRMED # Устаревший статус (для обратной совместимости)
|
||||
IN_TRANSIT # Устаревший статус (для обратной совместимости)
|
||||
SUPPLIER_APPROVED # Поставщик одобрил, ожидает подтверждения логистики
|
||||
LOGISTICS_CONFIRMED # Логистика подтвердила, ожидает отправки
|
||||
SHIPPED # Отправлено поставщиком, в пути
|
||||
DELIVERED # Доставлено и принято фулфилментом
|
||||
CANCELLED # Отменено (любой участник может отменить)
|
||||
PENDING # Ожидает одобрения поставщика
|
||||
CONFIRMED # Устаревший статус (для обратной совместимости)
|
||||
IN_TRANSIT # Устаревший статус (для обратной совместимости)
|
||||
SUPPLIER_APPROVED # Поставщик одобрил, ожидает подтверждения логистики
|
||||
LOGISTICS_CONFIRMED # Логистика подтвердила, ожидает отправки
|
||||
SHIPPED # Отправлено поставщиком, в пути
|
||||
DELIVERED # Доставлено и принято фулфилментом
|
||||
CANCELLED # Отменено (любой участник может отменить)
|
||||
}
|
||||
|
||||
input SupplyOrderInput {
|
||||
partnerId: ID!
|
||||
deliveryDate: DateTime!
|
||||
fulfillmentCenterId: ID # ID фулфилмент-центра для доставки
|
||||
logisticsPartnerId: ID! # ID логистической компании (обязательно)
|
||||
logisticsPartnerId: ID # ID логистической компании (опционально - может выбрать селлер или фулфилмент)
|
||||
items: [SupplyOrderItemInput!]!
|
||||
notes: String # Дополнительные заметки к заказу
|
||||
consumableType: String # Классификация расходников: FULFILLMENT_CONSUMABLES, SELLER_CONSUMABLES
|
||||
@ -642,6 +670,7 @@ export const typeDefs = gql`
|
||||
ourSupplyOrders: Int! # Расходники фулфилмента
|
||||
sellerSupplyOrders: Int! # Расходники селлеров
|
||||
incomingSupplierOrders: Int! # 🔔 Входящие заказы для поставщиков
|
||||
logisticsOrders: Int! # 🚚 Логистические заявки для логистики
|
||||
incomingRequests: Int!
|
||||
total: Int!
|
||||
}
|
||||
@ -819,6 +848,8 @@ export const typeDefs = gql`
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
middleName: String
|
||||
fullName: String
|
||||
name: String
|
||||
birthDate: DateTime
|
||||
avatar: String
|
||||
passportPhoto: String
|
||||
|
Reference in New Issue
Block a user