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

This commit is contained in:
Bivekich
2025-07-16 22:07:38 +03:00
parent 823ef9a28c
commit 205c9eae98
33 changed files with 3288 additions and 229 deletions

View File

@ -179,8 +179,30 @@ export const resolvers = {
const existingCounterpartyIds = existingCounterparties.map(c => c.counterpartyId)
const where: any = {
id: { not: currentUser.organization.id } // Исключаем только собственную организацию
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
const outgoingRequests = await prisma.counterpartyRequest.findMany({
where: {
senderId: currentUser.organization.id,
status: 'PENDING'
},
select: { receiverId: true }
})
const outgoingRequestIds = outgoingRequests.map(r => r.receiverId)
// Получаем входящие заявки для добавления флага hasIncomingRequest
const incomingRequests = await prisma.counterpartyRequest.findMany({
where: {
receiverId: currentUser.organization.id,
status: 'PENDING'
},
select: { senderId: true }
})
const incomingRequestIds = incomingRequests.map(r => r.senderId)
const where: Record<string, unknown> = {
// Больше не исключаем собственную организацию
}
if (args.type) {
@ -205,10 +227,13 @@ export const resolvers = {
}
})
// Добавляем флаг isCounterparty к каждой организации
// Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации
return organizations.map(org => ({
...org,
isCounterparty: existingCounterpartyIds.includes(org.id)
isCounterparty: existingCounterpartyIds.includes(org.id),
isCurrentUser: org.id === currentUser.organization?.id,
hasOutgoingRequest: outgoingRequestIds.includes(org.id),
hasIncomingRequest: incomingRequestIds.includes(org.id)
}))
},
@ -322,6 +347,82 @@ export const resolvers = {
},
orderBy: { createdAt: 'desc' }
})
},
// Сообщения с контрагентом
messages: async (_: unknown, args: { counterpartyId: string; limit?: number; offset?: number }, 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('У пользователя нет организации')
}
const limit = args.limit || 50
const offset = args.offset || 0
const messages = await prisma.message.findMany({
where: {
OR: [
{
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.counterpartyId
},
{
senderOrganizationId: args.counterpartyId,
receiverOrganizationId: currentUser.organization.id
}
]
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
},
receiverOrganization: {
include: {
users: true
}
}
},
orderBy: { createdAt: 'asc' },
take: limit,
skip: offset
})
return messages
},
// Список чатов (последние сообщения с каждым контрагентом)
conversations: 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('У пользователя нет организации')
}
// TODO: Здесь будет логика получения списка чатов
// Пока возвращаем пустой массив, так как таблица сообщений еще не создана
return []
}
},
@ -608,13 +709,16 @@ export const resolvers = {
})
}
// Создаем организацию селлера - используем название магазина как основное имя
const shopName = validationResults[0]?.data?.sellerName || 'Магазин'
// Создаем организацию селлера - используем tradeMark как основное имя
const tradeMark = validationResults[0]?.data?.tradeMark
const sellerName = validationResults[0]?.data?.sellerName
const shopName = tradeMark || sellerName || 'Магазин'
const organization = await prisma.organization.create({
data: {
inn: validationResults[0]?.data?.inn || `SELLER_${Date.now()}`,
name: shopName,
fullName: `Интернет-магазин "${shopName}"`,
name: shopName, // Используем tradeMark как основное название
fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`,
type: 'SELLER'
}
})
@ -858,11 +962,19 @@ export const resolvers = {
try {
const { input } = args
// Обновляем аватар пользователя если указан
// Обновляем данные пользователя (аватар, имя управляющего)
const userUpdateData: { avatar?: string; managerName?: string } = {}
if (input.avatar) {
userUpdateData.avatar = input.avatar
}
if (input.managerName) {
userUpdateData.managerName = input.managerName
}
if (Object.keys(userUpdateData).length > 0) {
await prisma.user.update({
where: { id: context.user.id },
data: { avatar: input.avatar }
data: userUpdateData
})
}
@ -874,6 +986,9 @@ export const resolvers = {
managementPost?: string
} = {}
// Название организации больше не обновляется через профиль
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
// Обновляем контактные данные в JSON поле phones
if (input.orgPhone) {
updateData.phones = [{ value: input.orgPhone, type: 'main' }]
@ -898,9 +1013,7 @@ export const resolvers = {
}
} = {}
if (input.managerName) {
customContacts.managerName = input.managerName
}
// managerName теперь сохраняется в поле пользователя, а не в JSON
if (input.telegram) {
customContacts.telegram = input.telegram
@ -1015,7 +1128,8 @@ export const resolvers = {
// Подготавливаем данные для обновления
const updateData: Prisma.OrganizationUpdateInput = {
kpp: organizationData.kpp,
name: organizationData.name,
// Для селлеров не обновляем название организации (это название магазина)
...(user.organization.type !== 'SELLER' && { name: organizationData.name }),
fullName: organizationData.fullName,
address: organizationData.address,
addressFull: organizationData.addressFull,
@ -1023,7 +1137,7 @@ export const resolvers = {
ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null,
registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null,
liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null,
managementName: organizationData.managementName,
managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null)
managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя
opfCode: organizationData.opfCode,
opfFull: organizationData.opfFull,
@ -1321,6 +1435,317 @@ export const resolvers = {
console.error('Error removing counterparty:', error)
return false
}
},
// Отправить сообщение
sendMessage: async (_: unknown, args: { receiverOrganizationId: string; content?: string; type?: 'TEXT' | 'VOICE' }, 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('У пользователя нет организации')
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
}
// Получаем организацию получателя
const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.receiverOrganizationId }
})
if (!receiverOrganization) {
throw new GraphQLError('Организация получателя не найдена')
}
try {
// Создаем сообщение
const message = await prisma.message.create({
data: {
content: args.content?.trim() || null,
type: args.type || 'TEXT',
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
},
receiverOrganization: {
include: {
users: true
}
}
}
})
return {
success: true,
message: 'Сообщение отправлено',
messageData: message
}
} catch (error) {
console.error('Error sending message:', error)
return {
success: false,
message: 'Ошибка при отправке сообщения'
}
}
},
// Отправить голосовое сообщение
sendVoiceMessage: async (_: unknown, args: { receiverOrganizationId: string; voiceUrl: string; voiceDuration: number }, 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('У пользователя нет организации')
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
}
// Получаем организацию получателя
const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.receiverOrganizationId }
})
if (!receiverOrganization) {
throw new GraphQLError('Организация получателя не найдена')
}
try {
// Создаем голосовое сообщение
const message = await prisma.message.create({
data: {
content: null,
type: 'VOICE',
voiceUrl: args.voiceUrl,
voiceDuration: args.voiceDuration,
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
},
receiverOrganization: {
include: {
users: true
}
}
}
})
return {
success: true,
message: 'Голосовое сообщение отправлено',
messageData: message
}
} catch (error) {
console.error('Error sending voice message:', error)
return {
success: false,
message: 'Ошибка при отправке голосового сообщения'
}
}
},
// Отправить изображение
sendImageMessage: async (_: unknown, args: { receiverOrganizationId: string; fileUrl: string; fileName: string; fileSize: number; fileType: 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('У пользователя нет организации')
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
}
try {
const message = await prisma.message.create({
data: {
content: null,
type: 'IMAGE',
fileUrl: args.fileUrl,
fileName: args.fileName,
fileSize: args.fileSize,
fileType: args.fileType,
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
},
receiverOrganization: {
include: {
users: true
}
}
}
})
return {
success: true,
message: 'Изображение отправлено',
messageData: message
}
} catch (error) {
console.error('Error sending image:', error)
return {
success: false,
message: 'Ошибка при отправке изображения'
}
}
},
// Отправить файл
sendFileMessage: async (_: unknown, args: { receiverOrganizationId: string; fileUrl: string; fileName: string; fileSize: number; fileType: 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('У пользователя нет организации')
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
}
try {
const message = await prisma.message.create({
data: {
content: null,
type: 'FILE',
fileUrl: args.fileUrl,
fileName: args.fileName,
fileSize: args.fileSize,
fileType: args.fileType,
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
},
receiverOrganization: {
include: {
users: true
}
}
}
})
return {
success: true,
message: 'Файл отправлен',
messageData: message
}
} catch (error) {
console.error('Error sending file:', error)
return {
success: false,
message: 'Ошибка при отправке файла'
}
}
},
// Отметить сообщения как прочитанные
markMessagesAsRead: async (_: unknown, args: { conversationId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
// TODO: Здесь будет логика обновления статуса сообщений
// Пока возвращаем успешный ответ
return true
}
},
@ -1359,5 +1784,23 @@ export const resolvers = {
return null
}
},
Message: {
type: (parent: { type?: string | null }) => {
return parent.type || 'TEXT'
},
createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString()
}
return parent.createdAt
},
updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString()
}
return parent.updatedAt
}
}
}