init
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user