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 }) }, }) }, })