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:
Marco Gallegos
2026-02-02 20:45:32 -06:00
parent 5f651f2a9d
commit 423f96022a
94 changed files with 17763 additions and 50 deletions

View 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>
`;
}