Добавлены новые зависимости для работы с эмодзи и улучшена структура базы данных. Реализована модель сообщений и обновлены компоненты для поддержки новых функций мессенджера. Обновлены запросы и мутации для работы с сообщениями и чатом.
This commit is contained in:
@ -355,4 +355,195 @@ export const REMOVE_COUNTERPARTY = gql`
|
||||
mutation RemoveCounterparty($organizationId: ID!) {
|
||||
removeCounterparty(organizationId: $organizationId)
|
||||
}
|
||||
`
|
||||
|
||||
// Мутации для сообщений
|
||||
export const SEND_MESSAGE = gql`
|
||||
mutation SendMessage($receiverOrganizationId: ID!, $content: String!, $type: MessageType = TEXT) {
|
||||
sendMessage(receiverOrganizationId: $receiverOrganizationId, content: $content, type: $type) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
id
|
||||
content
|
||||
type
|
||||
voiceUrl
|
||||
voiceDuration
|
||||
fileUrl
|
||||
fileName
|
||||
fileSize
|
||||
fileType
|
||||
senderId
|
||||
senderOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
receiverOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
isRead
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SEND_VOICE_MESSAGE = gql`
|
||||
mutation SendVoiceMessage($receiverOrganizationId: ID!, $voiceUrl: String!, $voiceDuration: Int!) {
|
||||
sendVoiceMessage(receiverOrganizationId: $receiverOrganizationId, voiceUrl: $voiceUrl, voiceDuration: $voiceDuration) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
id
|
||||
content
|
||||
type
|
||||
voiceUrl
|
||||
voiceDuration
|
||||
fileUrl
|
||||
fileName
|
||||
fileSize
|
||||
fileType
|
||||
senderId
|
||||
senderOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
receiverOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
isRead
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SEND_IMAGE_MESSAGE = gql`
|
||||
mutation SendImageMessage($receiverOrganizationId: ID!, $fileUrl: String!, $fileName: String!, $fileSize: Int!, $fileType: String!) {
|
||||
sendImageMessage(receiverOrganizationId: $receiverOrganizationId, fileUrl: $fileUrl, fileName: $fileName, fileSize: $fileSize, fileType: $fileType) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
id
|
||||
content
|
||||
type
|
||||
voiceUrl
|
||||
voiceDuration
|
||||
fileUrl
|
||||
fileName
|
||||
fileSize
|
||||
fileType
|
||||
senderId
|
||||
senderOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
receiverOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
isRead
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SEND_FILE_MESSAGE = gql`
|
||||
mutation SendFileMessage($receiverOrganizationId: ID!, $fileUrl: String!, $fileName: String!, $fileSize: Int!, $fileType: String!) {
|
||||
sendFileMessage(receiverOrganizationId: $receiverOrganizationId, fileUrl: $fileUrl, fileName: $fileName, fileSize: $fileSize, fileType: $fileType) {
|
||||
success
|
||||
message
|
||||
messageData {
|
||||
id
|
||||
content
|
||||
type
|
||||
voiceUrl
|
||||
voiceDuration
|
||||
fileUrl
|
||||
fileName
|
||||
fileSize
|
||||
fileType
|
||||
senderId
|
||||
senderOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
receiverOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
isRead
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const MARK_MESSAGES_AS_READ = gql`
|
||||
mutation MarkMessagesAsRead($conversationId: ID!) {
|
||||
markMessagesAsRead(conversationId: $conversationId)
|
||||
}
|
||||
`
|
@ -6,6 +6,7 @@ export const GET_ME = gql`
|
||||
id
|
||||
phone
|
||||
avatar
|
||||
managerName
|
||||
createdAt
|
||||
organization {
|
||||
id
|
||||
@ -61,9 +62,13 @@ export const SEARCH_ORGANIZATIONS = gql`
|
||||
emails
|
||||
createdAt
|
||||
isCounterparty
|
||||
isCurrentUser
|
||||
hasOutgoingRequest
|
||||
hasIncomingRequest
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,6 +81,7 @@ export const GET_MY_COUNTERPARTIES = gql`
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
managementName
|
||||
type
|
||||
address
|
||||
phones
|
||||
@ -84,6 +90,7 @@ export const GET_MY_COUNTERPARTIES = gql`
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,6 +112,11 @@ export const GET_INCOMING_REQUESTS = gql`
|
||||
address
|
||||
phones
|
||||
emails
|
||||
createdAt
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
receiver {
|
||||
id
|
||||
@ -112,6 +124,10 @@ export const GET_INCOMING_REQUESTS = gql`
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,6 +146,10 @@ export const GET_OUTGOING_REQUESTS = gql`
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
receiver {
|
||||
id
|
||||
@ -140,6 +160,11 @@ export const GET_OUTGOING_REQUESTS = gql`
|
||||
address
|
||||
phones
|
||||
emails
|
||||
createdAt
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,4 +191,84 @@ export const GET_ORGANIZATION = gql`
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Запросы для сообщений
|
||||
export const GET_MESSAGES = gql`
|
||||
query GetMessages($counterpartyId: ID!, $limit: Int, $offset: Int) {
|
||||
messages(counterpartyId: $counterpartyId, limit: $limit, offset: $offset) {
|
||||
id
|
||||
content
|
||||
type
|
||||
voiceUrl
|
||||
voiceDuration
|
||||
fileUrl
|
||||
fileName
|
||||
fileSize
|
||||
fileType
|
||||
senderId
|
||||
senderOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
receiverOrganization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
type
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
isRead
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_CONVERSATIONS = gql`
|
||||
query GetConversations {
|
||||
conversations {
|
||||
id
|
||||
counterparty {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
address
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
managerName
|
||||
}
|
||||
}
|
||||
lastMessage {
|
||||
id
|
||||
content
|
||||
type
|
||||
voiceUrl
|
||||
voiceDuration
|
||||
fileUrl
|
||||
fileName
|
||||
fileSize
|
||||
fileType
|
||||
senderId
|
||||
isRead
|
||||
createdAt
|
||||
}
|
||||
unreadCount
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,12 @@ export const typeDefs = gql`
|
||||
|
||||
# Исходящие заявки
|
||||
outgoingRequests: [CounterpartyRequest!]!
|
||||
|
||||
# Сообщения с контрагентом
|
||||
messages(counterpartyId: ID!, limit: Int, offset: Int): [Message!]!
|
||||
|
||||
# Список чатов (последние сообщения с каждым контрагентом)
|
||||
conversations: [Conversation!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -48,6 +54,13 @@ export const typeDefs = gql`
|
||||
respondToCounterpartyRequest(requestId: ID!, accept: Boolean!): CounterpartyRequestResponse!
|
||||
cancelCounterpartyRequest(requestId: ID!): Boolean!
|
||||
removeCounterparty(organizationId: ID!): Boolean!
|
||||
|
||||
# Работа с сообщениями
|
||||
sendMessage(receiverOrganizationId: ID!, content: String, type: MessageType = TEXT): MessageResponse!
|
||||
sendVoiceMessage(receiverOrganizationId: ID!, voiceUrl: String!, voiceDuration: Int!): MessageResponse!
|
||||
sendImageMessage(receiverOrganizationId: ID!, fileUrl: String!, fileName: String!, fileSize: Int!, fileType: String!): MessageResponse!
|
||||
sendFileMessage(receiverOrganizationId: ID!, fileUrl: String!, fileName: String!, fileSize: Int!, fileType: String!): MessageResponse!
|
||||
markMessagesAsRead(conversationId: ID!): Boolean!
|
||||
}
|
||||
|
||||
# Типы данных
|
||||
@ -55,6 +68,7 @@ export const typeDefs = gql`
|
||||
id: ID!
|
||||
phone: String!
|
||||
avatar: String
|
||||
managerName: String
|
||||
organization: Organization
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
@ -92,6 +106,9 @@ export const typeDefs = gql`
|
||||
users: [User!]!
|
||||
apiKeys: [ApiKey!]!
|
||||
isCounterparty: Boolean
|
||||
isCurrentUser: Boolean
|
||||
hasOutgoingRequest: Boolean
|
||||
hasIncomingRequest: Boolean
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
@ -224,6 +241,46 @@ export const typeDefs = gql`
|
||||
request: CounterpartyRequest
|
||||
}
|
||||
|
||||
# Типы для сообщений
|
||||
type Message {
|
||||
id: ID!
|
||||
content: String
|
||||
type: MessageType
|
||||
voiceUrl: String
|
||||
voiceDuration: Int
|
||||
fileUrl: String
|
||||
fileName: String
|
||||
fileSize: Int
|
||||
fileType: String
|
||||
senderId: ID!
|
||||
senderOrganization: Organization!
|
||||
receiverOrganization: Organization!
|
||||
isRead: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
enum MessageType {
|
||||
TEXT
|
||||
VOICE
|
||||
IMAGE
|
||||
FILE
|
||||
}
|
||||
|
||||
type Conversation {
|
||||
id: ID!
|
||||
counterparty: Organization!
|
||||
lastMessage: Message
|
||||
unreadCount: Int!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type MessageResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
messageData: Message
|
||||
}
|
||||
|
||||
# JSON скаляр
|
||||
scalar JSON
|
||||
`
|
Reference in New Issue
Block a user