/** * Accountability Roster PDF Generator * * Professional roster document for emergency preparedness. * Follows VIPSchedulePDF patterns for consistent styling. */ import { Document, Page, Text, View, StyleSheet, Font, Image, } from '@react-pdf/renderer'; import { PdfSettings } from '@/types/settings'; Font.register({ family: 'Helvetica', fonts: [ { src: 'Helvetica' }, { src: 'Helvetica-Bold', fontWeight: 'bold' }, ], }); interface VIP { id: string; name: string; organization: string | null; department: string; phone: string | null; email: string | null; emergencyContactName: string | null; emergencyContactPhone: string | null; isRosterOnly: boolean; partySize: number; } interface AccountabilityRosterPDFProps { vips: VIP[]; settings?: PdfSettings | null; } const createStyles = (accentColor: string = '#2c3e50', _pageSize: 'LETTER' | 'A4' = 'LETTER') => StyleSheet.create({ page: { padding: 40, paddingBottom: 80, fontSize: 9, fontFamily: 'Helvetica', backgroundColor: '#ffffff', color: '#333333', }, // Watermark watermark: { position: 'absolute', top: '40%', left: '50%', transform: 'translate(-50%, -50%) rotate(-45deg)', fontSize: 72, color: '#888888', opacity: 0.2, fontWeight: 'bold', zIndex: 0, }, // Logo logoContainer: { marginBottom: 10, flexDirection: 'row', justifyContent: 'center', }, logo: { maxWidth: 130, maxHeight: 50, objectFit: 'contain', }, // Header header: { marginBottom: 20, borderBottom: `2 solid ${accentColor}`, paddingBottom: 15, }, orgName: { fontSize: 9, color: '#7f8c8d', textTransform: 'uppercase', letterSpacing: 2, marginBottom: 6, }, title: { fontSize: 22, fontWeight: 'bold', color: accentColor, marginBottom: 4, }, subtitle: { fontSize: 10, color: '#7f8c8d', }, customMessage: { fontSize: 9, color: '#7f8c8d', marginTop: 8, padding: 8, backgroundColor: '#f8f9fa', borderLeft: `3 solid ${accentColor}`, }, timestampBar: { marginTop: 10, paddingTop: 8, borderTop: '1 solid #ecf0f1', flexDirection: 'row', justifyContent: 'space-between', }, timestamp: { fontSize: 7, color: '#95a5a6', }, // Summary stats row summaryRow: { flexDirection: 'row', marginBottom: 15, gap: 10, }, summaryCard: { flex: 1, padding: 10, backgroundColor: '#f8f9fa', borderLeft: `3 solid ${accentColor}`, }, summaryValue: { fontSize: 18, fontWeight: 'bold', color: '#2c3e50', }, summaryLabel: { fontSize: 8, color: '#7f8c8d', textTransform: 'uppercase', letterSpacing: 0.5, }, // Section sectionTitle: { fontSize: 10, fontWeight: 'bold', color: accentColor, textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8, paddingBottom: 4, borderBottom: `2 solid ${accentColor}`, }, section: { marginBottom: 18, }, // Table table: { borderLeft: '1 solid #dee2e6', borderRight: '1 solid #dee2e6', borderTop: '1 solid #dee2e6', }, tableHeader: { flexDirection: 'row', backgroundColor: accentColor, minHeight: 24, }, tableHeaderCell: { color: '#ffffff', fontSize: 7, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 0.5, padding: 6, justifyContent: 'center', }, tableRow: { flexDirection: 'row', borderBottom: '1 solid #dee2e6', minHeight: 28, }, tableRowAlt: { backgroundColor: '#f8f9fa', }, tableRowRoster: { backgroundColor: '#fef9e7', }, tableRowRosterAlt: { backgroundColor: '#fdf3d0', }, tableCell: { padding: 5, justifyContent: 'center', }, cellName: { fontSize: 9, fontWeight: 'bold', color: '#2c3e50', }, cellDept: { fontSize: 7, color: '#7f8c8d', marginTop: 1, }, cellText: { fontSize: 8, color: '#34495e', }, cellSmall: { fontSize: 7, color: '#7f8c8d', }, cellCenter: { fontSize: 9, fontWeight: 'bold', color: '#2c3e50', textAlign: 'center', }, cellNoData: { fontSize: 7, color: '#bdc3c7', fontStyle: 'italic', }, // Column widths colName: { width: '22%' }, colOrg: { width: '18%' }, colContact: { width: '22%' }, colEmergency: { width: '22%' }, colParty: { width: '8%' }, colNotes: { width: '8%' }, // Footer footer: { position: 'absolute', bottom: 25, left: 40, right: 40, paddingTop: 10, borderTop: '1 solid #dee2e6', }, footerContent: { flexDirection: 'row', justifyContent: 'space-between', }, footerLeft: { maxWidth: '60%', }, footerTitle: { fontSize: 8, fontWeight: 'bold', color: '#2c3e50', marginBottom: 3, }, footerContact: { fontSize: 7, color: '#7f8c8d', marginBottom: 1, }, footerRight: { textAlign: 'right', }, pageNumber: { fontSize: 7, color: '#95a5a6', }, // Empty state emptyState: { textAlign: 'center', padding: 30, color: '#95a5a6', fontSize: 11, }, }); const formatDepartment = (dept: string) => { switch (dept) { case 'OFFICE_OF_DEVELOPMENT': return 'Office of Dev'; case 'ADMIN': return 'Admin'; default: return dept; } }; export function AccountabilityRosterPDF({ vips, settings, }: AccountabilityRosterPDFProps) { const config = settings || { organizationName: 'VIP Transportation Services', accentColor: '#2c3e50', contactEmail: 'coordinator@example.com', contactPhone: '(555) 123-4567', contactLabel: 'Questions or Changes?', showDraftWatermark: false, showConfidentialWatermark: false, showTimestamp: true, showAppUrl: false, pageSize: 'LETTER' as const, logoUrl: null, tagline: null, headerMessage: null, footerMessage: null, secondaryContactName: null, secondaryContactPhone: null, }; const styles = createStyles(config.accentColor, config.pageSize); const generatedAt = new Date().toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', }); const activeVips = vips.filter((v) => !v.isRosterOnly).sort((a, b) => a.name.localeCompare(b.name)); const rosterOnlyVips = vips.filter((v) => v.isRosterOnly).sort((a, b) => a.name.localeCompare(b.name)); const totalPeople = vips.reduce((sum, v) => sum + (v.partySize || 1), 0); const activeCount = activeVips.reduce((sum, v) => sum + (v.partySize || 1), 0); const rosterCount = rosterOnlyVips.reduce((sum, v) => sum + (v.partySize || 1), 0); const renderTableHeader = () => ( Name Organization Contact Emergency Contact Party ); const renderVipRow = (vip: VIP, index: number, isRoster: boolean) => ( {vip.name} {formatDepartment(vip.department)} {vip.organization ? ( {vip.organization} ) : ( - )} {vip.phone && {vip.phone}} {vip.email && {vip.email}} {!vip.phone && !vip.email && No contact info} {vip.emergencyContactName ? ( <> {vip.emergencyContactName} {vip.emergencyContactPhone && ( {vip.emergencyContactPhone} )} ) : ( Not provided )} {vip.partySize} ); return ( {/* Watermarks */} {config.showDraftWatermark && ( DRAFT )} {config.showConfidentialWatermark && ( CONFIDENTIAL )} {/* Header */} {config.logoUrl && ( )} {config.organizationName} Accountability Roster Emergency Preparedness & Personnel Tracking {config.headerMessage && ( {config.headerMessage} )} {(config.showTimestamp || config.showAppUrl) && ( {config.showTimestamp && ( Generated: {generatedAt} )} {config.showAppUrl && ( Latest version: {typeof window !== 'undefined' ? window.location.origin : ''} )} )} {/* Summary Stats */} {totalPeople} Total People {activeCount} Active VIPs {rosterCount} Roster Only {/* Active VIPs Table */} {activeVips.length > 0 && ( Active VIPs ({activeVips.length} entries, {activeCount} people) {renderTableHeader()} {activeVips.map((vip, i) => renderVipRow(vip, i, false))} )} {/* Roster Only Table */} {rosterOnlyVips.length > 0 && ( Roster Only ({rosterOnlyVips.length} entries, {rosterCount} people) {renderTableHeader()} {rosterOnlyVips.map((vip, i) => renderVipRow(vip, i, true))} )} {/* Empty State */} {vips.length === 0 && ( No personnel records found. )} {/* Custom Footer Message */} {config.footerMessage && ( {config.footerMessage} )} {/* Footer */} {config.contactLabel} {config.contactEmail} {config.contactPhone} {config.secondaryContactName && ( {config.secondaryContactName} {config.secondaryContactPhone ? ` - ${config.secondaryContactPhone}` : ''} )} `Page ${pageNumber} of ${totalPages}` } /> ); }