Skip to content

Commit

Permalink
feat: appointments reminders
Browse files Browse the repository at this point in the history
  • Loading branch information
jorge210488 committed Jan 24, 2025
1 parent 5f63c44 commit db164c7
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 15 deletions.
79 changes: 79 additions & 0 deletions backend/src/nodemailer/nodemailer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,83 @@ export class NodemailerService {
console.error('Error enviando email: ', error)
}
}

async sendAppointmentReminder(
to: string,
subject: string,
appointmentDetails: {
given_name: string
start_time: string
end_time: string
appointment_type_id: string
provider_name: string
clinic_name: string
},
): Promise<void> {
const path = require('path')
const fs = require('fs')

const templatePath = path.join(
process.cwd(),
'src',
'templates',
'appointment-email.html',
)

if (!fs.existsSync(templatePath)) {
console.error(
`El archivo de plantilla no existe en la ruta: ${templatePath}`,
)
return
}

let htmlTemplate = fs.readFileSync(templatePath, 'utf8')

// Calcular duración en minutos
const startTime = new Date(appointmentDetails.start_time)
const endTime = new Date(appointmentDetails.end_time)
const durationInMinutes = Math.round(
(endTime.getTime() - startTime.getTime()) / (1000 * 60),
)

// Reemplazar los placeholders con los valores de appointmentDetails
htmlTemplate = htmlTemplate.replace(
'{{given_name}}',
appointmentDetails.given_name,
)
htmlTemplate = htmlTemplate.replace(
'{{start_time}}',
startTime.toLocaleString(),
)
htmlTemplate = htmlTemplate.replace(
'{{duration}}',
`${durationInMinutes} minutes`,
)
htmlTemplate = htmlTemplate.replace(
'{{appointment_type_id}}',
appointmentDetails.appointment_type_id,
)
htmlTemplate = htmlTemplate.replace(
'{{provider.name}}',
appointmentDetails.provider_name,
)
htmlTemplate = htmlTemplate.replace(
'{{clinic_name}}',
appointmentDetails.clinic_name,
)

const mailOptions = {
from: this.configService.get<string>('EMAIL_USER'),
to,
subject,
html: htmlTemplate,
}

try {
const info = await this.transporter.sendMail(mailOptions)
console.log(`Reminder email sent to ${to}. Response: ${info.response}`)
} catch (error) {
console.log('Error sending reminder email:', error.message)
}
}
}
1 change: 1 addition & 0 deletions backend/src/sms/sms.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import { SmsService } from './sms.service'
@Module({
controllers: [SmsController],
providers: [SmsService],
exports: [SmsService],
})
export class SmsModule {}
6 changes: 6 additions & 0 deletions backend/src/tasks/tasks.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { NotificationsModule } from '../notifications/notifications.module'
import { ScheduleModule } from '@nestjs/schedule'
import { ContactsModule } from 'src/contacts/contacts.module'
import { Clinic, ClinicSchema } from 'src/clinics/schemas/clinic.schema'
import { AppointmentsModule } from 'src/appointments/appointments.module'
import { NodemailerModule } from 'src/nodemailer/nodemailer.module'
import { SmsModule } from 'src/sms/sms.module'

@Module({
imports: [
Expand All @@ -17,6 +20,9 @@ import { Clinic, ClinicSchema } from 'src/clinics/schemas/clinic.schema'
]),
NotificationsModule,
ContactsModule,
AppointmentsModule,
NodemailerModule,
SmsModule,
],
controllers: [TasksController],
providers: [TasksService],
Expand Down
211 changes: 202 additions & 9 deletions backend/src/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,58 @@ import { CreateNotificationDto } from '../notifications/dtos/createNotification.
import { NotificationType } from '../notifications/enums/notifications.enum'
import { ContactsService } from '../contacts/contacts.service'
import { Clinic, ClinicDocument } from '../clinics/schemas/clinic.schema'
import { AppointmentsService } from 'src/appointments/appointments.service'
import { NodemailerService } from 'src/nodemailer/nodemailer.service'
import { SmsService } from 'src/sms/sms.service'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name)
private readonly frontendUrl: string

constructor(
@InjectModel(Clinic.name)
private readonly clinicModel: Model<ClinicDocument>,
private readonly notificationsService: NotificationsService,
private readonly contactsService: ContactsService,
) {}

@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) // Ejecuta la tarea diariamente a medianoche
async executeProfileCompletionTask(): Promise<void> {
this.logger.log('Executing profile completion task...')
private readonly appointmentsService: AppointmentsService,
private readonly nodemailerService: NodemailerService,
private readonly smsService: SmsService,
private readonly configService: ConfigService,
) {
this.frontendUrl = this.configService.get<string>('FRONTEND_URL')
if (!this.frontendUrl) {
this.logger.error(
'FRONTEND_URL is not defined in the environment variables.',
)
throw new Error('FRONTEND_URL must be defined in environment variables.')
}
}

// Obtener todas las clínicas
// Método para obtener todas las clínicas
private async getAllClinics(): Promise<ClinicDocument[]> {
const clinics = await this.clinicModel.find().exec()
if (!clinics.length) {
this.logger.log('No clinics found.')
return
return []
}

this.logger.log(`Found ${clinics.length} clinics.`)
return clinics
}

@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async executeProfileCompletionTask(): Promise<void> {
this.logger.log('Executing profile completion task...')

// Obtener todas las clínicas
const clinics = await this.getAllClinics()
if (!clinics.length) return

for (const clinic of clinics) {
try {
// Obtener todos los contactos de la clínica
const response = await this.contactsService.getContacts(clinic._id)
// console.log(contacts)
if (!response || !response.contacts || response.contacts.length === 0) {
this.logger.log(`No contacts found for clinic ${clinic._id}`)
continue
Expand Down Expand Up @@ -115,4 +137,175 @@ export class TasksService {

this.logger.log('Profile completion task completed.')
}

@Cron(CronExpression.EVERY_DAY_AT_NOON) // Se ejecutará diariamente al mediodía
async executeAppointmentsTask(): Promise<void> {
this.logger.log(
"Executing task to fetch tomorrow's appointments and send notifications...",
)

const clinics = await this.getAllClinics()
if (!clinics.length) return

for (const clinic of clinics) {
try {
const now = new Date()

// Calcular el rango para "mañana" en UTC
const startOfTomorrow = new Date()
startOfTomorrow.setUTCDate(now.getUTCDate() + 1)
startOfTomorrow.setUTCHours(0, 0, 0, 0)

const endOfTomorrow = new Date(startOfTomorrow)
endOfTomorrow.setUTCHours(23, 59, 59, 999)

// Formatear las fechas como 'yyyy-mm-ddTHH:mm:ssZ'
const formatToISO = (date: Date): string => date.toISOString()

const filter = `start_time > '${formatToISO(
startOfTomorrow,
)}' AND start_time < '${formatToISO(endOfTomorrow)}'`

this.logger.log(`Generated filter for clinic ${clinic._id}: ${filter}`)

// Llamar al método del servicio de citas
const appointmentsResponse =
await this.appointmentsService.getAppointments(clinic._id, { filter })

const appointments = appointmentsResponse?.appointments || []
this.logger.log(
`Fetched ${appointments.length} appointments for clinic ${clinic._id}.`,
)

if (!appointments.length) continue

// Enviar notificaciones para cada cita
for (const appointment of appointments) {
const contact = appointment.contact

if (!contact || !contact.remote_id) {
this.logger.warn(
`No contact information found for appointment ${appointment.name}.`,
)
continue
}

const notificationDto: CreateNotificationDto = {
remote_id: contact.remote_id,
clinic_id: clinic._id,
notification: {
title: 'Reminder for your appointment tomorrow!',
body: `You have an appointment scheduled on ${clinic.clinic_name} tomorrow. Please make sure to be on time.`,
image:
'https://res.cloudinary.com/deflfnoba/image/upload/v1736293681/DentalRainMaker%20Frontend/xpt6bwxwovvscuh3irci.png',
},
data: {
type: NotificationType.REMINDER,
},
webpush: {
fcm_options: {
link: `${this.frontendUrl}/patientDashboard/appointments`,
},
},
}

try {
// Enviar notificación push
await this.notificationsService.sendPushNotification(
notificationDto,
)

// Guardar la notificación en la base de datos
await this.notificationsService.createNotification(
notificationDto,
true,
)

this.logger.log(
`Notification sent to contact ${contact.remote_id} for appointment ${appointment.name}.`,
)
} catch (error) {
this.logger.error(
`Failed to send notification to contact ${contact.remote_id} for appointment ${appointment.name}: ${error.message}`,
)
}
}
for (const appointment of appointments) {
const contact = appointment.contact

if (!contact || !contact.remote_id) {
this.logger.warn(
`No contact information found for appointment ${appointment.name}.`,
)
continue
}

try {
// Obtener información del contacto por remote_id
const contactDetails = await this.contactsService.getContactById(
clinic._id,
contact.remote_id,
)

if (!contactDetails || !contactDetails.primary_email_address) {
this.logger.warn(
`No primary email found for contact ${contact.remote_id}.`,
)
continue
}

// Enviar el SMS CON TWILIO
const smsTo = contactDetails.phone_numbers?.[0]?.number
if (!smsTo) {
this.logger.warn(
`No phone number found for contact ${contactDetails.given_name}.`,
)
return
}

const smsBody = `Hello ${contactDetails.given_name}, this is a reminder for your appointment at ${clinic.clinic_name} for tomorrow.`
const sendSmsDto = {
to: smsTo,
body: smsBody,
}

await this.smsService.sendSms(sendSmsDto)

// ENVIO DE CORREO CON NODEMAILER
const emailTo = contactDetails.primary_email_address
const subject = 'Reminder: Your Appointment Tomorrow'

const appointmentDetails = {
given_name: contactDetails.given_name,
start_time: appointment.start_time,
end_time: appointment.end_time,
appointment_type_id: appointment.appointment_type_id,
provider_name:
appointment.providers?.[0]?.name || 'No provider available',
clinic_name: clinic.clinic_name,
}

console.log('body del correo', appointmentDetails)
await this.nodemailerService.sendAppointmentReminder(
emailTo,
subject,
appointmentDetails,
)

this.logger.log(
`Email reminder sent to ${emailTo} for appointment ${appointment.name}.`,
)
} catch (error) {
this.logger.error(
`Error processing contact ${contact.remote_id} for appointment ${appointment.name}: ${error.message}`,
)
}
}
} catch (error) {
this.logger.error(
`Error processing appointments for clinic ${clinic._id}: ${error.message}`,
)
}
}
}
}
10 changes: 4 additions & 6 deletions backend/src/templates/appointment-email.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,11 @@ <h1 class="title">
This is a friendly reminder of your upcoming appointment:<br /><br />

<span class="titleSee">Date:</span> {{start_time}}<br />
<span class="titleSee">Time:</span> {{start_time}} -
{{end_time}}<br />
<span class="titleSee">Time:</span> {{duration}}<br />
<span class="titleSee">Type:</span>
{{appointment_type_display_name}}<br />
<span class="titleSee">Location:</span> {{location}}<br />
<span class="titleSee">Doctor:</span>
{{doctor_display_name}}<br />
{{appointment_type_id}}<br />
<span class="titleSee">Provider:</span>
{{provider.name}}<br />
<span class="titleSee">Clinic:</span> {{clinic_name}}<br /><br />

Please make sure to arrive a few minutes early to ensure a smooth
Expand Down

0 comments on commit db164c7

Please sign in to comment.