359 lines
12 KiB
TypeScript
359 lines
12 KiB
TypeScript
import nodemailer from 'nodemailer';
|
|
import { prisma } from './db';
|
|
import { getSettings } from './settings';
|
|
|
|
export async function sendTicketEmail(bookingId: number) {
|
|
try {
|
|
// 1. Fetch active settings
|
|
const settings = await getSettings();
|
|
const {
|
|
brandName,
|
|
logoImageUrl,
|
|
logoIcon,
|
|
primaryColor,
|
|
smtpHost,
|
|
smtpPort,
|
|
smtpUser,
|
|
smtpPassword,
|
|
smtpSenderName,
|
|
smtpSenderEmail,
|
|
csPhone,
|
|
csWhatsapp,
|
|
csEmail,
|
|
} = settings;
|
|
|
|
// 2. Guard: Verify if SMTP is configured
|
|
if (!smtpHost || !smtpUser || !smtpPassword) {
|
|
console.warn(`[SMTP Email] SMTP is not fully configured (host: "${smtpHost}", user: "${smtpUser}"). Skipping email notification.`);
|
|
return false;
|
|
}
|
|
|
|
// 3. Fetch Booking with related Schedule and Route
|
|
const booking = await prisma.booking.findUnique({
|
|
where: { id: bookingId },
|
|
include: {
|
|
seats: true,
|
|
schedule: {
|
|
include: {
|
|
route: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!booking) {
|
|
console.error(`[SMTP Email] Booking with ID ${bookingId} not found.`);
|
|
return false;
|
|
}
|
|
|
|
if (!booking.passengerEmail) {
|
|
console.warn(`[SMTP Email] Passenger email is missing for booking ${booking.bookingCode}.`);
|
|
return false;
|
|
}
|
|
|
|
// 4. Set up nodemailer transporter
|
|
const port = Number(smtpPort) || 587;
|
|
const transporter = nodemailer.createTransport({
|
|
host: smtpHost,
|
|
port: port,
|
|
secure: port === 465, // true for 465, false for other ports
|
|
auth: {
|
|
user: smtpUser,
|
|
pass: smtpPassword,
|
|
},
|
|
});
|
|
|
|
// 5. Formats
|
|
const formatCurrency = (val: number) => {
|
|
return new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR',
|
|
maximumFractionDigits: 0,
|
|
}).format(val);
|
|
};
|
|
|
|
const formatDate = (date: Date) => {
|
|
return date.toLocaleDateString('id-ID', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const formatTime = (date: Date) => {
|
|
return date.toLocaleTimeString('id-ID', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
|
const ticketUrl = `${appUrl}/ticket/${booking.bookingCode}`;
|
|
const brandColorHex = primaryColor || '#6366f1';
|
|
|
|
// 6. Build HTML template with brand color matching theme settings
|
|
const htmlContent = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>E-Tiket Perjalanan Anda - ${booking.bookingCode}</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
background-color: #0b0f19;
|
|
color: #e2e8f0;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.email-wrapper {
|
|
max-width: 600px;
|
|
margin: 30px auto;
|
|
background: rgba(15, 23, 42, 0.9);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
|
}
|
|
.email-header {
|
|
background: linear-gradient(135deg, ${brandColorHex} 0%, #0f172a 100%);
|
|
padding: 30px 40px;
|
|
text-align: center;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
.brand-title {
|
|
color: #ffffff;
|
|
font-size: 24px;
|
|
font-weight: 800;
|
|
margin: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
.brand-logo {
|
|
font-size: 28px;
|
|
margin-right: 10px;
|
|
}
|
|
.email-body {
|
|
padding: 40px;
|
|
}
|
|
.greeting {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #ffffff;
|
|
margin-top: 0;
|
|
margin-bottom: 10px;
|
|
}
|
|
.intro-text {
|
|
color: #94a3b8;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
margin-bottom: 30px;
|
|
}
|
|
.ticket-panel {
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border: 1px dashed rgba(255, 255, 255, 0.15);
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.code-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
}
|
|
.code-value {
|
|
font-size: 28px;
|
|
font-weight: 800;
|
|
color: ${brandColorHex};
|
|
margin-top: 4px;
|
|
margin-bottom: 20px;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.grid-row {
|
|
display: flex;
|
|
margin-bottom: 16px;
|
|
}
|
|
.grid-col {
|
|
flex: 1;
|
|
}
|
|
.info-label {
|
|
font-size: 11px;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
margin-bottom: 4px;
|
|
}
|
|
.info-value {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #ffffff;
|
|
}
|
|
.route-flow {
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
margin-top: 20px;
|
|
padding-top: 20px;
|
|
}
|
|
.city-title {
|
|
font-size: 16px;
|
|
font-weight: 800;
|
|
color: #ffffff;
|
|
}
|
|
.time-sub {
|
|
font-size: 12px;
|
|
color: ${brandColorHex};
|
|
font-weight: 600;
|
|
}
|
|
.seat-badge {
|
|
display: inline-block;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
color: #ffffff;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
margin-right: 4px;
|
|
}
|
|
.btn-container {
|
|
text-align: center;
|
|
margin: 35px 0 15px 0;
|
|
}
|
|
.btn-action {
|
|
display: inline-block;
|
|
background-color: ${brandColorHex};
|
|
color: #ffffff !important;
|
|
text-decoration: none;
|
|
padding: 14px 30px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
box-shadow: 0 4px 14px color-mix(in srgb, ${brandColorHex} 30%, transparent);
|
|
transition: all 0.2s ease;
|
|
}
|
|
.email-footer {
|
|
background: #020617;
|
|
padding: 30px 40px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
text-align: center;
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
.footer-links {
|
|
margin-top: 12px;
|
|
font-size: 11px;
|
|
}
|
|
.footer-link {
|
|
color: #94a3b8;
|
|
text-decoration: none;
|
|
margin: 0 8px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="email-wrapper">
|
|
<div class="email-header">
|
|
<h1 class="brand-title">
|
|
<span class="brand-logo">${logoIcon}</span>
|
|
${brandName}
|
|
</h1>
|
|
</div>
|
|
|
|
<div class="email-body">
|
|
<p class="greeting">Halo ${booking.passengerName},</p>
|
|
<p class="intro-text">
|
|
Pembayaran Anda untuk pemesanan tiket perjalanan telah berhasil dikonfirmasi. Berikut adalah rincian e-ticket resmi Anda:
|
|
</p>
|
|
|
|
<div class="ticket-panel">
|
|
<div class="code-label">Kode Booking Perjalanan</div>
|
|
<div class="code-value">${booking.bookingCode}</div>
|
|
|
|
<div class="grid-row">
|
|
<div class="grid-col">
|
|
<div class="info-label">Nama Penumpang</div>
|
|
<div class="info-value">${booking.passengerName}</div>
|
|
</div>
|
|
<div class="grid-col">
|
|
<div class="info-label">Kursi Terpilih</div>
|
|
<div class="info-value">
|
|
${booking.seats.map(s => `<span class="seat-badge">${s.seatNumber}</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-row">
|
|
<div class="grid-col">
|
|
<div class="info-label">Jenis Armada</div>
|
|
<div class="info-value">${booking.schedule.vehicleType}</div>
|
|
</div>
|
|
<div class="grid-col">
|
|
<div class="info-label">Total Pembayaran</div>
|
|
<div class="info-value" style="color: #10b981;">${formatCurrency(Number(booking.totalPrice))}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="route-flow">
|
|
<div class="grid-row" style="margin-bottom: 0;">
|
|
<div class="grid-col">
|
|
<div class="info-label">Keberangkatan</div>
|
|
<div class="city-title">${booking.schedule.route.departureCity}</div>
|
|
<div class="time-sub">${formatTime(booking.schedule.departureTime)}</div>
|
|
<div style="font-size: 12px; color: #94a3b8; margin-top: 2px;">${formatDate(booking.schedule.departureTime)}</div>
|
|
</div>
|
|
<div class="grid-col" style="text-align: center; display: flex; align-items: center; justify-content: center; max-width: 50px;">
|
|
<span style="font-size: 20px; color: #64748b;">➔</span>
|
|
</div>
|
|
<div class="grid-col" style="text-align: right;">
|
|
<div class="info-label">Tujuan</div>
|
|
<div class="city-title">${booking.schedule.route.arrivalCity}</div>
|
|
<div class="time-sub">${formatTime(booking.schedule.arrivalTime)}</div>
|
|
<div style="font-size: 12px; color: #94a3b8; margin-top: 2px;">${formatDate(booking.schedule.arrivalTime)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="intro-text" style="text-align: center;">
|
|
Silakan bawa Kode Booking atau tunjukkan boarding pass digital Anda saat keberangkatan di lokasi loket resmi kami.
|
|
</p>
|
|
|
|
<div class="btn-container">
|
|
<a href="${ticketUrl}" target="_blank" class="btn-action">Lihat Boarding Pass Digital</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="email-footer">
|
|
<p>Terima kasih telah bepergian bersama kami!</p>
|
|
<p>Butuh bantuan? CS Phone: ${csPhone} | WhatsApp: ${csWhatsapp} | Email: ${csEmail}</p>
|
|
<div class="footer-links">
|
|
<a href="${appUrl}" class="footer-link">Beranda</a> |
|
|
<a href="${appUrl}/dashboard" class="footer-link">Dashboard Saya</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
// 7. Send the email
|
|
const senderName = smtpSenderName || brandName;
|
|
const senderEmail = smtpSenderEmail || smtpUser;
|
|
|
|
await transporter.sendMail({
|
|
from: `"${senderName}" <${senderEmail}>`,
|
|
to: booking.passengerEmail,
|
|
subject: `[E-Tiket Lunas] Boarding Pass Perjalanan Anda - ${booking.bookingCode}`,
|
|
html: htmlContent,
|
|
});
|
|
|
|
console.log(`[SMTP Email] Email receipt successfully sent to ${booking.passengerEmail} for booking ${booking.bookingCode}.`);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('[SMTP Email] Error sending email receipt:', error);
|
|
return false;
|
|
}
|
|
}
|