Files
travel-antarkota/src/lib/email.ts
T
2026-06-18 12:16:27 +07:00

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