mirror of
https://github.com/marcogll/gloria_app.git
synced 2026-03-15 14:24:44 +00:00
feat: Complete Sprints 3 & 4 - Email, Reschedule, OCR, Upload, Contact Forms
Sprint 3 - Crisis y Agenda (100%): - Implement SMTP email service with nodemailer - Create email templates (reschedule, daily agenda, course inquiry) - Add appointment reschedule functionality with modal - Add Google Calendar updateEvent function - Create scheduled job for daily agenda email at 10 PM - Add manual trigger endpoint for testing Sprint 4 - Pagos y Roles (100%): - Add Payment proof upload with OCR (tesseract.js, pdf-parse) - Extract data from proofs (amount, date, reference, sender, bank) - Create PaymentUpload component with drag & drop - Add course contact form to /cursos page - Update Services button to navigate to /servicios - Add Reschedule button to Assistant and Therapist dashboards - Add PaymentUpload component to Assistant dashboard - Add eventId field to Appointment model - Add OCR-extracted fields to Payment model - Update Prisma schema and generate client - Create API endpoints for reschedule, upload-proof, courses contact - Create manual trigger endpoint for daily agenda job - Initialize daily agenda job in layout.tsx Dependencies added: - nodemailer, node-cron, tesseract.js, sharp, pdf-parse, @types/nodemailer Files created/modified: - src/infrastructure/email/smtp.ts - src/lib/email/templates/* - src/jobs/send-daily-agenda.ts - src/app/api/calendar/reschedule/route.ts - src/app/api/payments/upload-proof/route.ts - src/app/api/contact/courses/route.ts - src/app/api/jobs/trigger-agenda/route.ts - src/components/dashboard/RescheduleModal.tsx - src/components/dashboard/PaymentUpload.tsx - src/components/forms/CourseContactForm.tsx - src/app/dashboard/asistente/page.tsx (updated) - src/app/dashboard/terapeuta/page.tsx (updated) - src/app/cursos/page.tsx (updated) - src/components/layout/Services.tsx (updated) - src/infrastructure/external/calendar.ts (updated) - src/app/layout.tsx (updated) - prisma/schema.prisma (updated) - src/lib/validations.ts (updated) - src/lib/env.ts (updated) Tests: - TypeScript typecheck: No errors - ESLint: Only warnings (img tags, react-hooks) - Production build: Successful Documentation: - Updated CHANGELOG.md with Sprint 3/4 changes - Updated PROGRESS.md with 100% completion status - Updated TASKS.md with completed tasks
This commit is contained in:
95
src/infrastructure/email/smtp.ts
Normal file
95
src/infrastructure/email/smtp.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import { getEnv } from "@/lib/env";
|
||||
import { getRescheduleConfirmationTemplate } from "@/lib/email/templates/reschedule-confirmation";
|
||||
import { getDailyAgendaTemplate } from "@/lib/email/templates/daily-agenda";
|
||||
import { getCourseInquiryTemplate } from "@/lib/email/templates/course-inquiry";
|
||||
|
||||
let transporter: nodemailer.Transporter | null = null;
|
||||
|
||||
function getTransporter(): nodemailer.Transporter {
|
||||
if (transporter) {
|
||||
return transporter;
|
||||
}
|
||||
|
||||
const env = getEnv();
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: env.SMTP_USER,
|
||||
pass: env.SMTP_PASS,
|
||||
},
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100,
|
||||
});
|
||||
|
||||
return transporter;
|
||||
}
|
||||
|
||||
export async function sendEmail(to: string, subject: string, html: string): Promise<void> {
|
||||
const env = getEnv();
|
||||
const transporter = getTransporter();
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${env.SMTP_FROM_NAME}" <${env.SMTP_FROM_EMAIL}>`,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
};
|
||||
|
||||
let retries = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
await transporter.sendMail(mailOptions);
|
||||
console.log(`[SMTP] Email sent to ${to}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
retries++;
|
||||
if (retries === maxRetries) {
|
||||
console.error(`[SMTP] Failed to send email after ${maxRetries} retries:`, error);
|
||||
throw error;
|
||||
}
|
||||
const delay = Math.pow(2, retries) * 1000;
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendRescheduleConfirmation(
|
||||
to: string,
|
||||
patientName: string,
|
||||
oldDate: Date,
|
||||
newDate: Date
|
||||
): Promise<void> {
|
||||
const html = getRescheduleConfirmationTemplate(patientName, oldDate, newDate);
|
||||
await sendEmail(to, "Confirmación de Cambio de Cita - Gloria Niño Terapia", html);
|
||||
}
|
||||
|
||||
export async function sendDailyAgenda(to: string, appointments: any[]): Promise<void> {
|
||||
const html = getDailyAgendaTemplate(appointments);
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const formattedDate = tomorrow.toLocaleDateString("es-MX", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
await sendEmail(to, `📅 Agenda para el día ${formattedDate}`, html);
|
||||
}
|
||||
|
||||
export async function sendCourseInquiry(
|
||||
to: string,
|
||||
name: string,
|
||||
email: string,
|
||||
course: string,
|
||||
message: string
|
||||
): Promise<void> {
|
||||
const html = getCourseInquiryTemplate(name, email, course, message);
|
||||
await sendEmail(to, `🎓 Nueva Consulta sobre Cursos - ${course}`, html);
|
||||
}
|
||||
Reference in New Issue
Block a user