mirror of
https://github.com/marcogll/gloria_app.git
synced 2026-03-15 10:24:43 +00:00
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
250 lines
6.9 KiB
TypeScript
250 lines
6.9 KiB
TypeScript
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>
|
|
`;
|
|
}
|