import { schema } from 'nexus'
import * as MeetingParticipantController from '../../controllers/meetingParticipant'
import moment from 'moment'
import * as Auth from '../../utils/auth'
import * as dot from 'dot-object'
import config from '../../config'
import * as ZoomUtils from '../../utils/zoom'
import { logger } from '../../utils/logger'
import * as EmailUtils from '../../utils/email'
import GeneralConstants from '../../constants/general'
import { prismaClient as prisma } from '../../prismaClient'
const dynamicFields = [
{
path: 'firstName',
regex: new RegExp('@First name', 'g'),
},
{
path: 'lastName',
regex: new RegExp('@Last name', 'g'),
},
{
path: 'email',
regex: new RegExp('@Email', 'g'),
},
{
path: 'copropertyDueAmount',
regex: new RegExp(
'@Coproperty due amount',
'g'
),
},
{
path: 'percentageSquareFootage',
regex: new RegExp('@Quote part', 'g'),
},
{
path: 'registrationUrl',
regex: new RegExp(
'@Registration url',
'g'
),
},
{
path: 'joinUrl',
regex: new RegExp(
'@Participant join url',
'g'
),
},
]
// const SUBJECT_DEFAULT_FR_EN = 'Assemblée virtuelle du [DATE] / Virtual assembly on [DATE]'
// const SUBJECT_DEFAULT_FR = ''
// const SUBJECT_DEFAULT_EN = ''
// const BODY_DEFAULT_FR_EN = `Bonjour Thierry`
// const BODY_DEFAULT_FR = ''
// const BODY_DEFAULT_EN = ''
async function inviteMeetingParticipant({ meetingId, meetingParticipantId }) {
const meeting = await prisma.meeting.findOne({
where: { id: meetingId },
select: {
registrationUrl: true,
invitationSubject: true,
invitationBody: true,
invitationAttachments: {
select: {
id: true,
name: true,
url: true,
},
},
project: {
select: {
id: true,
name: true,
colorHex: true,
logo: {
select: {
url: true,
},
},
managingCompany: {
select: {
shortName: true,
longName: true,
logo: {
select: {
url: true,
},
},
},
},
},
},
},
})
const subject = meeting.invitationSubject
let body = meeting.invitationBody
const attachments = meeting.invitationAttachments
if (!subject) {
throw new Error('Subject cannot be empty')
}
if (!body) {
throw new Error('Body cannot be empty')
}
const meetingParticipant = await prisma.meetingParticipant.findOne({
where: { id: meetingParticipantId },
select: {
id: true,
proportionateShare: true,
user: {
select: {
id: true,
preferedLanguage: true,
firstName: true,
lastName: true,
email: true,
properties: {
select: {
id: true,
building: {
select: {
project: {
select: {
id: true,
},
},
},
},
},
},
},
},
},
})
const user = meetingParticipant.user
dynamicFields.forEach((dynamicField) => {
let value
if (dynamicField.path === 'firstName') {
value = dot.pick(dynamicField.path, user)
} else if (dynamicField.path === 'lastName') {
value = dot.pick(dynamicField.path, user)
} else if (dynamicField.path === 'email') {
value = dot.pick(dynamicField.path, user)
} else if (dynamicField.path === 'copropertyDueAmount') {
value = dot.pick(dynamicField.path, '-') // TO DO FIX
} else if (dynamicField.path === 'percentageSquareFootage') {
value = dot.pick(dynamicField.path, meetingParticipant.proportionateShare)
} else if (dynamicField.path === 'registrationUrl') {
value = dot.pick(dynamicField.path, meeting)
} else if (dynamicField.path === 'joinUrl') {
// Only use so that after registering, the user is redirected to a property of the meeting's project
const userSinglePropertyForProject = user.properties.find(
(property) => property.building?.project?.id === meeting.project.id
)
const residentPortalUrl = `${config.residentWebHostName}/auth/register?email=${user.email}&property=${userSinglePropertyForProject?.id}&item=${meetingId}&module=assemblies`
if (user.preferedLanguage === 'fr') {
value = `Join the assembly platform`
} else {
value = `Joindre le portail de la réunion`
}
}
body = body.replace(dynamicField.regex, value)
})
await EmailUtils.send({
to: [{ email: user.email }],
template: 'RESIDENT_GENERAL_NOTIFICATION',
attachments: attachments.map(({ name, url }) => ({
url,
filename: name,
})),
from: {
email: GeneralConstants.EMAIL_COMMUNICATION_WALTER_FROM_EMAIL,
name:
meeting.project.managingCompany.shortName ||
meeting.project.managingCompany.longName ||
GeneralConstants.EMAIL_COMMUNICATION_WALTER_FROM_NAME,
},
subject: subject,
customVariables: {
projectColor: meeting.project.colorHex || '#000000',
projectName: meeting.project.name,
projectLogoUrl: meeting.project.logo?.url,
managingCompanyLogoUrl: meeting.project.managingCompany.logo?.url,
title: subject,
body: body,
bodySplitted: [{ text: body }],
dontShowDownloadWalter: true,
},
})
await prisma.meetingParticipant.update({
where: { id: meetingParticipantId },
data: {
receivedInvitation: true,
receivedInvitationAt: new Date(),
activities: {
create: [
{
text: 'Received meeting invitation',
},
],
},
},
})
}
export const Meeting = schema.objectType({
name: 'Meeting',
definition(t) {
t.model.id()
t.model.createdAt()
t.model.updatedAt()
t.model.date()
t.model.zoomWebinarPassword()
t.model.topic()
t.model.agenda()
t.model.start()
t.model.password()
t.model.durationMinutes()
t.model.contactName()
t.model.contactEmail()
t.model.status()
t.model.joinUrl()
t.model.registrationUrl()
t.model.startUrl()
t.model.startedAt()
t.model.endedAt()
t.model.invitationSubject()
t.model.invitationBody()
t.model.zoomWebinarUuid()
t.model.project()
t.model.participants()
t.model.participantsUnits()
t.model.invitationAttachments()
t.model.polls()
},
})
export const MeetingMutation = schema.extendType({
type: 'Mutation',
definition: (t) => {
t.crud.deleteOneMeeting({ alias: 'deleteMeeting' })
t.crud.updateOneMeeting({ alias: 'updateMeeting' })
t.crud.createOneMeeting({ alias: 'createMeeting' })
t.field('createMeeting', {
type: 'Meeting',
args: {
data: schema.arg({ type: 'MeetingCreateInput' }),
},
resolve: async (parent, { data }, ctx: NexusContext) => {
const currentUserId = Auth.getUserId(ctx)
const currentUserManagingCompany = await ctx.prisma.user
.findOne({ where: { id: currentUserId } })
.managingCompany()
let createdMeeting
let webinar
try {
createdMeeting = await ctx.prisma.meeting.create({
data: {
...data,
},
})
const { users: zoomUsers } = await ZoomUtils.getUsers(
currentUserManagingCompany.zoomAccessToken
)
const zoomUsersWithSettings: any[] = await Promise.all(
zoomUsers.map(async (user) => ({
id: user.id, // Need id for when creating the webinar just below. The rest of the data we don't care...
...(await ZoomUtils.getUserSettings({
accessToken: currentUserManagingCompany.zoomAccessToken,
userId: user.id,
})),
}))
)
const zoomUserWithWebinarFeature = zoomUsersWithSettings.find(
(user) => user.feature.webinar
)
if (!zoomUserWithWebinarFeature) {
throw new Error(
"You don't have any users that have Webinar feature in Zoom. Please upgrade your Zoom account first"
)
}
webinar = await ZoomUtils.createWebinar({
accessToken: currentUserManagingCompany.zoomAccessToken,
userId: zoomUserWithWebinarFeature.id,
topic: createdMeeting.topic,
agenda: createdMeeting.agenda,
start: moment.utc(createdMeeting.start).format(),
password: createdMeeting.password,
contactName: createdMeeting.contactName,
contactEmail: createdMeeting.contactEmail,
})
// Updating our meeting to have Zoom information
await ctx.prisma.meeting.update({
where: { id: createdMeeting.id },
data: {
zoomWebinarUuid: webinar.uuid,
zoomWebinarId: String(webinar.id),
joinUrl: webinar.join_url,
registrationUrl: webinar.registration_url,
startUrl: webinar.start_url,
},
})
const meeting = await ctx.prisma.meeting.findOne({
where: {
id: createdMeeting.id,
},
include: {
participants: {
include: {
user: true,
},
},
},
})
const meetingParticipants = meeting.participants
// Create and approve registrants
for await (const meetingParticipant of meetingParticipants) {
await MeetingParticipantController.createAndApproveMeetingParticipant({
accessToken: currentUserManagingCompany.zoomAccessToken,
meetingParticipant,
zoomWebinarId: webinar.id,
})
}
} catch (error) {
if (createdMeeting) {
await ctx.prisma.meeting.delete({ where: { id: createdMeeting.id } })
}
if (webinar) {
await ZoomUtils.deleteWebinar({
webinarId: webinar.id,
accessToken: currentUserManagingCompany.zoomAccessToken,
})
}
throw error
}
return createdMeeting
},
})
t.field('deleteMeeting', {
type: 'Meeting',
args: {
where: schema.arg({ type: 'MeetingWhereUniqueInput' }),
},
resolve: async (_, { where }, ctx: NexusContext) => {
const currentUserId = Auth.getUserId(ctx)
const currentUserManagingCompany = await ctx.prisma.user
.findOne({ where: { id: currentUserId } })
.managingCompany()
const meetingToDelete = await ctx.prisma.meeting.findOne({ where })
if (meetingToDelete.zoomWebinarId) {
try {
await ZoomUtils.deleteWebinar({
accessToken: currentUserManagingCompany.zoomAccessToken,
webinarId: meetingToDelete.zoomWebinarId,
})
} catch (error) {
logger.error(error)
}
}
return ctx.prisma.meeting.delete({ where })
},
})
t.field('updateMeeting', {
type: 'Meeting',
args: {
data: schema.arg({ type: 'MeetingUpdateInput' }),
where: schema.arg({ type: 'MeetingWhereUniqueInput' }),
},
resolve: async (_, { where, data }, ctx: NexusContext) => {
const currentUserId = Auth.getUserId(ctx)
const currentUserManagingCompany = await ctx.prisma.user
.findOne({ where: { id: currentUserId } })
.managingCompany()
const updatedMeeting = await ctx.prisma.meeting.update({ where, data })
if (
data.agenda ||
data.topic ||
data.password ||
data.durationMinutes ||
data.contactEmail ||
data.contactName ||
data.start
) {
await ZoomUtils.updateWebinar({
accessToken: currentUserManagingCompany.zoomAccessToken,
webinarId: updatedMeeting.zoomWebinarId,
data: {
topic: updatedMeeting.topic,
agenda: updatedMeeting.agenda,
start: moment.utc(updatedMeeting.start).format(),
password: updatedMeeting.password,
durationMinutes: updatedMeeting.durationMinutes,
contactName: updatedMeeting.contactName,
contactEmail: updatedMeeting.contactEmail,
},
})
}
return updatedMeeting
},
})
t.field('sendInvitationToAllParticipant', {
type: 'Boolean',
args: {
meetingId: schema.stringArg(),
},
resolve: async (_, { meetingId }, ctx: NexusContext) => {
const meetingParticipants = await ctx.prisma.meeting
.findOne({
where: { id: meetingId },
})
.participants()
await Promise.all(
meetingParticipants.map(async (meetingParticipant) => {
try {
await inviteMeetingParticipant({
meetingId,
meetingParticipantId: meetingParticipant.id,
})
} catch (error) {
logger.debug(error)
}
})
)
return true
},
})
t.field('sendInvitationToParticipant', {
type: 'Boolean',
args: {
participantId: schema.stringArg(),
meetingId: schema.stringArg(),
},
resolve: async (_, { participantId, meetingId }) => {
await inviteMeetingParticipant({
meetingId,
meetingParticipantId: participantId,
})
return true
},
})
t.field('createRegistrantsOnZoom', {
type: 'Boolean',
args: {
participantId: schema.stringArg(),
meetingId: schema.stringArg(),
},
resolve: async (_, { participantId, meetingId }) => {
await inviteMeetingParticipant({
meetingId,
meetingParticipantId: participantId,
})
return true
},
})
},
})
export const MeetingQuery = schema.extendType({
type: 'Query',
definition: (t) => {
t.crud.meeting()
t.crud.meetings({ filtering: true, ordering: true })
t.field('getMeetingPollResults', {
type: 'Meeting',
args: { where: schema.arg({ type: 'MeetingWhereUniqueInput' }) },
resolve: async (_, { where }, ctx: NexusContext) => {
return ctx.prisma.meeting.findOne({ where })
},
})
},
})