mirror of
https://github.com/marcogll/gloria_app.git
synced 2026-03-16 10:25:08 +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:
155
src/lib/email/templates/course-inquiry.ts
Normal file
155
src/lib/email/templates/course-inquiry.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
export function getCourseInquiryTemplate(
|
||||
name: string,
|
||||
email: string,
|
||||
course: string,
|
||||
message: string
|
||||
): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nueva Consulta sobre Cursos</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #7C3AED 0%, #9333EA 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.header .subtitle {
|
||||
margin-top: 10px;
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
}
|
||||
.content {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.info-card {
|
||||
background: #F9FAFB;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.info-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #6B7280;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.value {
|
||||
font-size: 16px;
|
||||
color: #111827;
|
||||
font-weight: 500;
|
||||
}
|
||||
.message-box {
|
||||
background: #F3E8FF;
|
||||
border-left: 4px solid #7C3AED;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.message-box .label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.message-text {
|
||||
font-size: 15px;
|
||||
color: #374151;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.cta {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.cta a {
|
||||
display: inline-block;
|
||||
background: #7C3AED;
|
||||
color: white;
|
||||
padding: 12px 30px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.cta a:hover {
|
||||
background: #6D28D9;
|
||||
}
|
||||
.footer {
|
||||
background: #F9FAFB;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎓 Nueva Consulta sobre Cursos</h1>
|
||||
<div class="subtitle">${course}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Has recibido una nueva consulta sobre uno de tus cursos.</p>
|
||||
|
||||
<div class="info-card">
|
||||
<div class="info-item">
|
||||
<div class="label">Nombre</div>
|
||||
<div class="value">${name}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">Email</div>
|
||||
<div class="value">${email}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">Curso de Interés</div>
|
||||
<div class="value">${course}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-box">
|
||||
<div class="label">Mensaje</div>
|
||||
<div class="message-text">${message}</div>
|
||||
</div>
|
||||
|
||||
<div class="cta">
|
||||
<a href="mailto:${email}?subject=Re: Consulta sobre ${course}">Responder al Interesado</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Gloria Niño - Plataforma de Gestión Terapéutica</p>
|
||||
<p style="margin-top: 5px;">Este mensaje fue enviado automáticamente</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
249
src/lib/email/templates/daily-agenda.ts
Normal file
249
src/lib/email/templates/daily-agenda.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
interface Appointment {
|
||||
date: Date;
|
||||
patient: {
|
||||
name: string;
|
||||
phone: string;
|
||||
};
|
||||
isCrisis: boolean;
|
||||
payment: {
|
||||
status: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function getDailyAgendaTemplate(appointments: Appointment[]): string {
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString("es-MX", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString("es-MX", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const getPaymentStatus = (payment: { status: string } | null) => {
|
||||
if (!payment) return '<span style="color: #F59E0B;">Pendiente</span>';
|
||||
switch (payment.status) {
|
||||
case "APPROVED":
|
||||
return '<span style="color: #10B981;">Aprobado</span>';
|
||||
case "REJECTED":
|
||||
return '<span style="color: #EF4444;">Rechazado</span>';
|
||||
default:
|
||||
return '<span style="color: #F59E0B;">Pendiente</span>';
|
||||
}
|
||||
};
|
||||
|
||||
const getCrisisBadge = (isCrisis: boolean) => {
|
||||
if (isCrisis) {
|
||||
return '<span style="display: inline-block; padding: 2px 8px; background: #FEF2F2; color: #DC2626; border-radius: 4px; font-size: 11px; font-weight: 600; margin-left: 5px;">CRISIS</span>';
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const pendingPayments = appointments.filter(
|
||||
(a) => !a.payment || a.payment.status === "PENDING"
|
||||
).length;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Agenda Diaria</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #7C3AED 0%, #9333EA 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.date {
|
||||
margin-top: 10px;
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
}
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
padding: 30px;
|
||||
background: #F9FAFB;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
.summary-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.summary-card h3 {
|
||||
margin: 0;
|
||||
font-size: 36px;
|
||||
color: #7C3AED;
|
||||
}
|
||||
.summary-card p {
|
||||
margin: 5px 0 0;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
.no-appointments {
|
||||
text-align: center;
|
||||
padding: 60px 30px;
|
||||
color: #6B7280;
|
||||
}
|
||||
.no-appointments h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 24px;
|
||||
color: #374151;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 12px 15px;
|
||||
background: #F9FAFB;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid #E5E7EB;
|
||||
}
|
||||
td {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
tr:hover {
|
||||
background: #F9FAFB;
|
||||
}
|
||||
.time {
|
||||
font-weight: 600;
|
||||
color: #7C3AED;
|
||||
font-size: 15px;
|
||||
}
|
||||
.patient-name {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
.patient-phone {
|
||||
color: #6B7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
.type {
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer {
|
||||
background: #F9FAFB;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📅 Agenda del Día</h1>
|
||||
<div class="date">${formatDate(tomorrow)}</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
appointments.length > 0
|
||||
? `
|
||||
<div class="summary">
|
||||
<div class="summary-card">
|
||||
<h3>${appointments.length}</h3>
|
||||
<p>Total Citas</p>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>${pendingPayments}</h3>
|
||||
<p>Pagos Pendientes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hora</th>
|
||||
<th>Paciente</th>
|
||||
<th>Teléfono</th>
|
||||
<th>Tipo</th>
|
||||
<th>Pago</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${appointments
|
||||
.map(
|
||||
(apt) => `
|
||||
<tr>
|
||||
<td class="time">${formatTime(apt.date)}</td>
|
||||
<td>
|
||||
<div class="patient-name">${apt.patient.name}${getCrisisBadge(apt.isCrisis)}</div>
|
||||
</td>
|
||||
<td><span class="patient-phone">${apt.patient.phone}</span></td>
|
||||
<td><span class="type">${apt.isCrisis ? "Crisis" : "Regular"}</span></td>
|
||||
<td>${getPaymentStatus(apt.payment)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="content">
|
||||
<div class="no-appointments">
|
||||
<h2>🎉</h2>
|
||||
<p>No hay citas programadas para mañana</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<div class="footer">
|
||||
<p>Gloria Niño - Plataforma de Gestión Terapéutica</p>
|
||||
<p style="margin-top: 5px;">Este reporte se genera automáticamente a las 10 PM</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
134
src/lib/email/templates/reschedule-confirmation.ts
Normal file
134
src/lib/email/templates/reschedule-confirmation.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
export function getRescheduleConfirmationTemplate(
|
||||
patientName: string,
|
||||
oldDate: Date,
|
||||
newDate: Date
|
||||
): string {
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString("es-MX", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Confirmación de Cambio de Cita</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #7C3AED 0%, #9333EA 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.content {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.greeting {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
color: #7C3AED;
|
||||
}
|
||||
.appointment-card {
|
||||
background: #F9FAFB;
|
||||
border-left: 4px solid #7C3AED;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.appointment-card.cancelled {
|
||||
border-left-color: #EF4444;
|
||||
background: #FEF2F2;
|
||||
}
|
||||
.appointment-card.new {
|
||||
border-left-color: #10B981;
|
||||
background: #ECFDF5;
|
||||
}
|
||||
.label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #6B7280;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.date {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
.footer {
|
||||
background: #F9FAFB;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
.contact-info {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #F3E8FF;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Confirmación de Cambio de Cita</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="greeting">Hola, ${patientName}</p>
|
||||
<p>Tu cita ha sido reacomodada exitosamente. Aquí están los detalles:</p>
|
||||
|
||||
<div class="appointment-card cancelled">
|
||||
<div class="label">Cita Cancelada</div>
|
||||
<div class="date">${formatDate(oldDate)}</div>
|
||||
</div>
|
||||
|
||||
<div class="appointment-card new">
|
||||
<div class="label">Nueva Cita Confirmada</div>
|
||||
<div class="date">${formatDate(newDate)}</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info">
|
||||
<p><strong>¿Necesitas hacer más cambios?</strong></p>
|
||||
<p>Por favor contáctanos lo antes posible para confirmar o realizar modificaciones adicionales.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Gloria Niño - Terapia Integral</p>
|
||||
<p style="margin-top: 5px;">Este mensaje fue enviado automáticamente</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user