Добавлены новые поля в модель Supply и обновлены компоненты для работы с расходниками. Реализована логика загрузки и отображения чатов с непрочитанными сообщениями в мессенджере. Обновлены запросы и мутации GraphQL для поддержки новых полей. Исправлены ошибки отображения и добавлены индикаторы для непрочитанных сообщений.

This commit is contained in:
Bivekich
2025-07-21 14:34:12 +03:00
parent f71262577a
commit a3fc7d969f
10 changed files with 514 additions and 125 deletions

View File

@ -533,9 +533,81 @@ export const resolvers = {
throw new GraphQLError("У пользователя нет организации");
}
// TODO: Здесь будет логика получения списка чатов
// Пока возвращаем пустой массив, так как таблица сообщений еще не создана
return [];
// Получаем всех контрагентов
const counterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
include: {
counterparty: {
include: {
users: true,
},
},
},
});
// Для каждого контрагента получаем последнее сообщение и количество непрочитанных
const conversations = await Promise.all(
counterparties.map(async (cp) => {
const counterpartyId = cp.counterparty.id;
// Последнее сообщение с этим контрагентом
const lastMessage = await prisma.message.findFirst({
where: {
OR: [
{
senderOrganizationId: currentUser.organization!.id,
receiverOrganizationId: counterpartyId,
},
{
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization!.id,
},
],
},
include: {
sender: true,
senderOrganization: {
include: {
users: true,
},
},
receiverOrganization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: "desc" },
});
// Количество непрочитанных сообщений от этого контрагента
const unreadCount = await prisma.message.count({
where: {
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization!.id,
isRead: false,
},
});
// Если есть сообщения с этим контрагентом, включаем его в список
if (lastMessage) {
return {
id: `${currentUser.organization!.id}-${counterpartyId}`,
counterparty: cp.counterparty,
lastMessage,
unreadCount,
updatedAt: lastMessage.createdAt,
};
}
return null;
})
);
// Фильтруем null значения и сортируем по времени последнего сообщения
return conversations
.filter((conv) => conv !== null)
.sort((a, b) => new Date(b!.updatedAt).getTime() - new Date(a!.updatedAt).getTime());
},
// Мои услуги
@ -2410,8 +2482,34 @@ export const resolvers = {
});
}
// TODO: Здесь будет логика обновления статуса сообщений
// Пока возвращаем успешный ответ
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// conversationId имеет формат "currentOrgId-counterpartyId"
const [, counterpartyId] = args.conversationId.split('-');
if (!counterpartyId) {
throw new GraphQLError("Неверный ID беседы");
}
// Помечаем все непрочитанные сообщения от контрагента как прочитанные
await prisma.message.updateMany({
where: {
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization.id,
isRead: false,
},
data: {
isRead: true,
},
});
return true;
},
@ -2594,6 +2692,14 @@ export const resolvers = {
name: string;
description?: string;
price: number;
quantity: number;
unit: string;
category: string;
status: string;
date: string;
supplier: string;
minStock: number;
currentStock: number;
imageUrl?: string;
};
},
@ -2627,7 +2733,14 @@ export const resolvers = {
name: args.input.name,
description: args.input.description,
price: args.input.price,
quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
quantity: args.input.quantity,
unit: args.input.unit,
category: args.input.category,
status: args.input.status,
date: new Date(args.input.date),
supplier: args.input.supplier,
minStock: args.input.minStock,
currentStock: args.input.currentStock,
imageUrl: args.input.imageUrl,
organizationId: currentUser.organization.id,
},
@ -2657,6 +2770,14 @@ export const resolvers = {
name: string;
description?: string;
price: number;
quantity: number;
unit: string;
category: string;
status: string;
date: string;
supplier: string;
minStock: number;
currentStock: number;
imageUrl?: string;
};
},
@ -2696,7 +2817,14 @@ export const resolvers = {
name: args.input.name,
description: args.input.description,
price: args.input.price,
quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
quantity: args.input.quantity,
unit: args.input.unit,
category: args.input.category,
status: args.input.status,
date: new Date(args.input.date),
supplier: args.input.supplier,
minStock: args.input.minStock,
currentStock: args.input.currentStock,
imageUrl: args.input.imageUrl,
},
include: { organization: true },
@ -2912,9 +3040,37 @@ export const resolvers = {
},
});
// Создаем расходники на основе заказанных товаров
const suppliesData = args.input.items.map((item) => {
const product = products.find((p) => p.id === item.productId)!;
const productWithCategory = supplyOrder.items.find(
(orderItem) => orderItem.productId === item.productId
)?.product;
return {
name: product.name,
description: product.description || `Заказано у ${partner.name}`,
price: product.price,
quantity: item.quantity,
unit: "шт",
category: productWithCategory?.category?.name || "Упаковка",
status: "in-transit", // Статус "в пути" так как заказ только создан
date: new Date(args.input.deliveryDate),
supplier: partner.name || partner.fullName || "Не указан",
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
currentStock: 0, // Пока товар не пришел
organizationId: currentUser.organization!.id,
};
});
// Создаем расходники
await prisma.supply.createMany({
data: suppliesData,
});
return {
success: true,
message: "Заказ поставки создан успешно",
message: `Заказ поставки создан успешно! Добавлено ${suppliesData.length} расходников в каталог.`,
order: supplyOrder,
};
} catch (error) {
@ -3000,7 +3156,7 @@ export const resolvers = {
weight: args.input.weight,
dimensions: args.input.dimensions,
material: args.input.material,
images: args.input.images || [],
images: JSON.stringify(args.input.images || []),
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
organizationId: currentUser.organization.id,
@ -3111,7 +3267,7 @@ export const resolvers = {
weight: args.input.weight,
dimensions: args.input.dimensions,
material: args.input.material,
images: args.input.images || [],
images: args.input.images ? JSON.stringify(args.input.images) : undefined,
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
},
@ -4213,6 +4369,25 @@ export const resolvers = {
},
},
Product: {
images: (parent: { images: unknown }) => {
// Если images это строка JSON, парсим её в массив
if (typeof parent.images === 'string') {
try {
return JSON.parse(parent.images);
} catch {
return [];
}
}
// Если это уже массив, возвращаем как есть
if (Array.isArray(parent.images)) {
return parent.images;
}
// Иначе возвращаем пустой массив
return [];
},
},
Message: {
type: (parent: { type?: string | null }) => {
return parent.type || "TEXT";