// PDF Ticket Generation Service import PDFDocument from 'pdfkit'; import QRCode from 'qrcode'; interface TicketData { id: string; qrCode: string; attendeeName: string; attendeeEmail?: string; event: { title: string; startDatetime: string; endDatetime?: string; location: string; locationUrl?: string; }; timezone?: string; } /** * Generate a QR code as a data URL */ async function generateQRCode(data: string): Promise { return QRCode.toBuffer(data, { type: 'png', width: 200, margin: 2, errorCorrectionLevel: 'M', }); } /** * Format date for display using site timezone */ function formatDate(dateStr: string, timezone: string = 'America/Asuncion'): string { const date = new Date(dateStr); return date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: timezone, }); } /** * Format time for display using site timezone */ function formatTime(dateStr: string, timezone: string = 'America/Asuncion'): string { const date = new Date(dateStr); return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, timeZone: timezone, }); } /** * Generate a PDF ticket for a single ticket */ export async function generateTicketPDF(ticket: TicketData): Promise { return new Promise(async (resolve, reject) => { try { const doc = new PDFDocument({ size: 'A4', margin: 50, }); const chunks: Buffer[] = []; doc.on('data', (chunk: Buffer) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); const frontendUrl = process.env.FRONTEND_URL || 'https://spanglishcommunity.com'; // Generate QR code with ticket URL const qrUrl = `${frontendUrl}/ticket/${ticket.id}`; const qrBuffer = await generateQRCode(qrUrl); // ==================== Header ==================== doc.fontSize(28).fillColor('#1a1a1a').text('Spanglish', { align: 'center' }); doc.moveDown(0.5); doc.fontSize(12).fillColor('#666').text('Language Exchange Community', { align: 'center' }); // Divider line doc.moveDown(1); doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); doc.moveDown(1); // ==================== Event Info ==================== doc.fontSize(22).fillColor('#1a1a1a').text(ticket.event.title, { align: 'center' }); doc.moveDown(0.5); // Date and time (using site timezone) const tz = ticket.timezone || 'America/Asuncion'; doc.fontSize(14).fillColor('#333'); doc.text(formatDate(ticket.event.startDatetime, tz), { align: 'center' }); const startTime = formatTime(ticket.event.startDatetime, tz); const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime, tz) : null; const timeRange = endTime ? `${startTime} - ${endTime}` : startTime; doc.text(timeRange, { align: 'center' }); doc.moveDown(0.5); doc.fontSize(12).fillColor('#666').text(ticket.event.location, { align: 'center' }); // ==================== QR Code ==================== doc.moveDown(2); // Center the QR code const qrSize = 180; const pageWidth = 595; // A4 width in points const qrX = (pageWidth - qrSize) / 2; doc.image(qrBuffer, qrX, doc.y, { width: qrSize, height: qrSize }); doc.y += qrSize + 10; // ==================== Attendee Info ==================== doc.moveDown(1); doc.fontSize(16).fillColor('#1a1a1a').text(ticket.attendeeName, { align: 'center' }); if (ticket.attendeeEmail) { doc.fontSize(10).fillColor('#888').text(ticket.attendeeEmail, { align: 'center' }); } // ==================== Ticket ID ==================== doc.moveDown(1); doc.fontSize(9).fillColor('#aaa').text(`Ticket ID: ${ticket.id}`, { align: 'center' }); doc.text(`Code: ${ticket.qrCode}`, { align: 'center' }); // ==================== Footer ==================== doc.moveDown(2); doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); doc.moveDown(0.5); doc.fontSize(10).fillColor('#888').text('Scan this QR code at the entrance', { align: 'center' }); doc.moveDown(0.3); doc.fontSize(8).fillColor('#aaa').text('This ticket is non-transferable. One scan per entry.', { align: 'center' }); doc.end(); } catch (error) { reject(error); } }); } /** * Generate a combined PDF with multiple tickets */ export async function generateCombinedTicketsPDF(tickets: TicketData[]): Promise { return new Promise(async (resolve, reject) => { try { const doc = new PDFDocument({ size: 'A4', margin: 50, }); const chunks: Buffer[] = []; doc.on('data', (chunk: Buffer) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); const frontendUrl = process.env.FRONTEND_URL || 'https://spanglishcommunity.com'; for (let i = 0; i < tickets.length; i++) { const ticket = tickets[i]; if (i > 0) { doc.addPage(); } // Generate QR code const qrUrl = `${frontendUrl}/ticket/${ticket.id}`; const qrBuffer = await generateQRCode(qrUrl); // ==================== Header ==================== doc.fontSize(28).fillColor('#1a1a1a').text('Spanglish', { align: 'center' }); doc.moveDown(0.5); doc.fontSize(12).fillColor('#666').text('Language Exchange Community', { align: 'center' }); // Divider line doc.moveDown(1); doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); doc.moveDown(1); // ==================== Event Info ==================== doc.fontSize(22).fillColor('#1a1a1a').text(ticket.event.title, { align: 'center' }); doc.moveDown(0.5); // Date and time (using site timezone) const tz = ticket.timezone || 'America/Asuncion'; doc.fontSize(14).fillColor('#333'); doc.text(formatDate(ticket.event.startDatetime, tz), { align: 'center' }); const startTime = formatTime(ticket.event.startDatetime, tz); const endTime = ticket.event.endDatetime ? formatTime(ticket.event.endDatetime, tz) : null; const timeRange = endTime ? `${startTime} - ${endTime}` : startTime; doc.text(timeRange, { align: 'center' }); doc.moveDown(0.5); doc.fontSize(12).fillColor('#666').text(ticket.event.location, { align: 'center' }); // ==================== QR Code ==================== doc.moveDown(2); const qrSize = 180; const pageWidth = 595; const qrX = (pageWidth - qrSize) / 2; doc.image(qrBuffer, qrX, doc.y, { width: qrSize, height: qrSize }); doc.y += qrSize + 10; // ==================== Attendee Info ==================== doc.moveDown(1); doc.fontSize(16).fillColor('#1a1a1a').text(ticket.attendeeName, { align: 'center' }); if (ticket.attendeeEmail) { doc.fontSize(10).fillColor('#888').text(ticket.attendeeEmail, { align: 'center' }); } // ==================== Ticket ID ==================== doc.moveDown(1); doc.fontSize(9).fillColor('#aaa').text(`Ticket ID: ${ticket.id}`, { align: 'center' }); doc.text(`Code: ${ticket.qrCode}`, { align: 'center' }); // Ticket number for multi-ticket bookings if (tickets.length > 1) { doc.text(`Ticket ${i + 1} of ${tickets.length}`, { align: 'center' }); } // ==================== Footer ==================== doc.moveDown(2); doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e0e0e0').stroke(); doc.moveDown(0.5); doc.fontSize(10).fillColor('#888').text('Scan this QR code at the entrance', { align: 'center' }); doc.moveDown(0.3); doc.fontSize(8).fillColor('#aaa').text('This ticket is non-transferable. One scan per entry.', { align: 'center' }); } doc.end(); } catch (error) { reject(error); } }); } export default { generateTicketPDF, generateCombinedTicketsPDF, };