import { jsPDF } from 'jspdf' import { db } from './db.js' import { getActiveMasterKey } from './auth.js' import { getLogbookKey } from './logbookKeys.js' import { decryptJson } from './crypto.js' import { isSignatureImage, isPasskeySignature, isClassicSignature, getSignaturePayload } from '../utils/signatures.js' import { sortLogEventsByTime } from '../utils/logEntryPayload.js' import i18n from '../i18n/index.js' import { formatAppDateTime } from '../utils/dateTimeFormat.js' function formatPasskeySignDate(signedAt: string): string { return formatAppDateTime(signedAt, i18n.language) } export async function generateLogbookPagePdf(logbookId: string, entryId: string, preloadedData?: { yacht: any; entry: any }): Promise { let yachtName = '', owner = '', homePort = '', registration = '', callsign = '', atis = '', mmsi = ''; let entry: any = null; if (preloadedData) { const yacht = preloadedData.yacht || {}; yachtName = yacht.name || ''; owner = yacht.owner || ''; homePort = yacht.port || ''; registration = yacht.registrationNumber || yacht.registration || ''; callsign = yacht.callSign || ''; atis = yacht.atis || ''; mmsi = yacht.mmsi || ''; entry = preloadedData.entry; } else { const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) { throw new Error('Encryption key not found. Please log in.') } const { resolveVesselForLogbook } = await import('./resolveVessel.js') const yacht = await resolveVesselForLogbook(logbookId) if (yacht) { yachtName = yacht.name || '' owner = yacht.owner || '' homePort = yacht.homePort || '' registration = yacht.registrationNumber || '' callsign = yacht.callSign || '' atis = yacht.atis || '' mmsi = yacht.mmsi || '' } // 2. Fetch active Entry const entryRecord = await db.entries.get(entryId); if (!entryRecord) { throw new Error('Entry not found'); } entry = await decryptJson(entryRecord.encryptedData, entryRecord.iv, entryRecord.tag, masterKey); } if (!entry) { throw new Error('Failed to load entry'); } // Create PDF landscape A4 const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' }); // Setup Styles doc.setFont('Helvetica', 'normal'); // --- DRAW HEADER SECTION --- doc.setFontSize(14); doc.setFont('Helvetica', 'bold'); doc.text('OFFIZIELLES SCHIFFSLOGBUCH (AC NAUTIK STANDARD)', 10, 15); doc.setFontSize(8.5); doc.setFont('Helvetica', 'normal'); doc.text(`Yachtname: ${yachtName || '—'}`, 10, 21); doc.text(`Eigner: ${owner || '—'}`, 55, 21); doc.text(`Heimathafen: ${homePort || '—'}`, 100, 21); doc.text(`Kennzeichen: ${registration || '—'}`, 145, 21); doc.text(`Rufzeichen: ${callsign || '—'}`, 190, 21); doc.text(`ATIS: ${atis || '—'}`, 230, 21); doc.text(`MMSI: ${mmsi || '—'}`, 260, 21); doc.text(`Datum: ${entry.date || '—'}`, 10, 24); doc.text(`Reisetag: ${entry.dayOfTravel || '—'}`, 60, 24); doc.text(`Reise von (Departure): ${entry.departure || '—'}`, 110, 24); doc.text(`nach (Destination): ${entry.destination || '—'}`, 200, 24); // Format Crew names with initials const crewSnapshots = (entry.crewSnapshotsById as Record) || {} const crewList: string[] = [] if (entry.selectedSkipperId && crewSnapshots[entry.selectedSkipperId]) { const name = crewSnapshots[entry.selectedSkipperId].name || 'Skipper' const initial = name.trim().split(/\s+/)[0]?.charAt(0).toUpperCase() || 'S' crewList.push(`${name} [${initial}] (Skipper)`) } else if (crewSnapshots['skipper']) { const name = crewSnapshots['skipper'].name || 'Skipper' crewList.push(`${name} [S] (Skipper)`) } if (Array.isArray(entry.selectedCrewIds)) { for (const crewId of entry.selectedCrewIds) { const snap = crewSnapshots[crewId] if (snap) { const name = snap.name || '' const initial = name.trim().split(/\s+/)[0]?.charAt(0).toUpperCase() || '?' crewList.push(`${name} [${initial}]`) } } } const crewText = crewList.length > 0 ? `Besatzung (Crew): ${crewList.join(', ')}` : '' doc.setFont('Helvetica', 'normal'); if (entry.trackDistanceNm) { doc.text( `GPS-Track: ${entry.trackDistanceNm} sm · max. ${entry.trackSpeedMaxKn ?? '—'} kn · Ø ${entry.trackSpeedAvgKn ?? '—'} kn`, 10, 27 ); if (crewText) { doc.text(crewText, 140, 27); } } else if (crewText) { doc.text(crewText, 10, 27); } // Divider line doc.setLineWidth(0.3); doc.line(10, 29, 287, 29); // --- DRAW EVENTS TABLE --- doc.setFont('Helvetica', 'bold'); doc.setFontSize(9); doc.text('CHRONOLOGISCHES EREIGNISPROTOKOLL / EVENT JOURNAL', 10, 34); // Table Headers const colWidths = [12, 10, 10, 12, 12, 13, 10, 12, 10, 15, 12, 45, 94]; // Total = 277mm const colHeaders = [ 'Zeit', 'MgK', 'rwK', 'Wind Dir', 'Wind Str', 'Druck', 'See', 'Strom', 'Lage', 'Segel/Motor', 'Log', 'GPS Position', 'Bemerkungen / Vorkommnisse' ]; let startY = 37; let rowHeight = 6; doc.setFontSize(7.5); // Draw Header Row let currentX = 10; doc.setFillColor(240, 240, 240); doc.rect(10, startY, 277, rowHeight, 'F'); doc.rect(10, startY, 277, rowHeight, 'S'); for (let i = 0; i < colHeaders.length; i++) { doc.text(colHeaders[i], currentX + 1, startY + 4.2); currentX += colWidths[i]; } // Draw Data Rows const events = entry.events || []; const maxRows = 16; const sortedEvents = sortLogEventsByTime(events); doc.setFont('Helvetica', 'normal'); for (let rowIndex = 0; rowIndex < maxRows; rowIndex++) { const y = startY + rowHeight + (rowIndex * rowHeight); // Draw row outline doc.rect(10, y, 277, rowHeight, 'S'); // Draw vertical column cell dividers let cellX = 10; for (let colIdx = 0; colIdx < colWidths.length - 1; colIdx++) { cellX += colWidths[colIdx]; doc.line(cellX, y, cellX, y + rowHeight); } const ev = sortedEvents[rowIndex]; if (ev) { let writeX = 10; doc.text(ev.time || '', writeX + 1, y + 4.2); writeX += colWidths[0]; doc.text(ev.mgk ? `${ev.mgk}°` : '—', writeX + 1, y + 4.2); writeX += colWidths[1]; doc.text(ev.rwk ? `${ev.rwk}°` : '—', writeX + 1, y + 4.2); writeX += colWidths[2]; doc.text(ev.windDirection || '—', writeX + 1, y + 4.2); writeX += colWidths[3]; doc.text(ev.windStrength || '—', writeX + 1, y + 4.2); writeX += colWidths[4]; doc.text(ev.windPressure ? `${ev.windPressure} hPa` : '—', writeX + 1, y + 4.2); writeX += colWidths[5]; doc.text(ev.seaState || '—', writeX + 1, y + 4.2); writeX += colWidths[6]; doc.text(ev.current || '—', writeX + 1, y + 4.2); writeX += colWidths[7]; doc.text(ev.heel ? `${ev.heel}°` : '—', writeX + 1, y + 4.2); writeX += colWidths[8]; doc.text(ev.sailsOrMotor || '—', writeX + 1, y + 4.2); writeX += colWidths[9]; doc.text(ev.logReading ? `${ev.logReading} sm` : '—', writeX + 1, y + 4.2); writeX += colWidths[10]; const gps = ev.gpsLat && ev.gpsLng ? `${ev.gpsLat}, ${ev.gpsLng}` : '—'; doc.text(gps, writeX + 1, y + 4.2); writeX += colWidths[11]; const crewSnapshots = (entry.crewSnapshotsById as Record) || {}; let initial = ''; if (ev.creatorId) { const snap = crewSnapshots[ev.creatorId]; let name = ''; if (snap) { name = snap.name || ''; } else if (ev.creatorId === 'skipper') { name = 'Skipper'; } else { name = ev.creatorId; } if (name) { initial = name.trim().split(/\s+/)[0]?.charAt(0).toUpperCase() || ''; } } // Clip remarks to fit within the 94mm bounds let remarks = ev.remarks || ''; if (initial) { remarks = `[${initial}] ${remarks}`; } const maxChars = 65; const clippedRemarks = remarks.length > maxChars ? remarks.substring(0, maxChars) + '...' : remarks; doc.text(clippedRemarks, writeX + 1, y + 4.2); } } // --- DRAW FOOTER SECTION --- const footerY = startY + rowHeight + (maxRows * rowHeight) + 4; // Consumables (Water & Diesel) doc.setFont('Helvetica', 'bold'); doc.setFontSize(8.5); doc.text('VERBRAUCHSWERTE / CONSUMPTON STATS', 10, footerY + 3); let fwY = footerY + 5; const tankRows = 4; doc.rect(10, fwY, 110, rowHeight * tankRows, 'S'); doc.line(10, fwY + rowHeight, 120, fwY + rowHeight); doc.line(10, fwY + rowHeight * 2, 120, fwY + rowHeight * 2); doc.line(10, fwY + rowHeight * 3, 120, fwY + rowHeight * 3); doc.line(40, fwY, 40, fwY + rowHeight * tankRows); doc.line(60, fwY, 60, fwY + rowHeight * tankRows); doc.line(80, fwY, 80, fwY + rowHeight * tankRows); doc.line(100, fwY, 100, fwY + rowHeight * tankRows); doc.setFont('Helvetica', 'bold'); doc.setFontSize(7.5); doc.text('Betriebsmittel', 11, fwY + 4.2); doc.text('Morgen (L)', 41, fwY + 4.2); doc.text('Nachgefüllt', 61, fwY + 4.2); doc.text('Abend (L)', 81, fwY + 4.2); doc.text('Verbrauch', 101, fwY + 4.2); doc.setFont('Helvetica', 'normal'); doc.text('Frischwasser', 11, fwY + rowHeight + 4.2); doc.text(String(entry.freshwater?.morning ?? '0'), 41, fwY + rowHeight + 4.2); doc.text(String(entry.freshwater?.refilled ?? '0'), 61, fwY + rowHeight + 4.2); doc.text(String(entry.freshwater?.evening ?? '0'), 81, fwY + rowHeight + 4.2); doc.text(String(entry.freshwater?.consumption ?? '0'), 101, fwY + rowHeight + 4.2); doc.text('Treibstoff (Fuel)', 11, fwY + rowHeight * 2 + 4.2); doc.text(String(entry.fuel?.morning ?? '0'), 41, fwY + rowHeight * 2 + 4.2); doc.text(String(entry.fuel?.refilled ?? '0'), 61, fwY + rowHeight * 2 + 4.2); doc.text(String(entry.fuel?.evening ?? '0'), 81, fwY + rowHeight * 2 + 4.2); doc.text(String(entry.fuel?.consumption ?? '0'), 101, fwY + rowHeight * 2 + 4.2); doc.text('Grauwasser', 11, fwY + rowHeight * 3 + 4.2); doc.text('—', 41, fwY + rowHeight * 3 + 4.2); doc.text('—', 61, fwY + rowHeight * 3 + 4.2); doc.text(String(entry.greywater?.level ?? '0'), 81, fwY + rowHeight * 3 + 4.2); doc.text('—', 101, fwY + rowHeight * 3 + 4.2); // Signatures Box let sigX = 130; let sigY = footerY + 5; doc.setFont('Helvetica', 'bold'); doc.text('FREIGABE & UNTERSCHRIFTEN / SIGNATURES', sigX, footerY + 3); doc.rect(sigX, sigY, 157, rowHeight * 3, 'S'); doc.line(sigX, sigY + rowHeight * 1.5, sigX + 157, sigY + rowHeight * 1.5); doc.line(sigX + 78.5, sigY, sigX + 78.5, sigY + rowHeight * 3); doc.text('Skipper Unterschrift:', sigX + 2, sigY + 4.2); if (isPasskeySignature(entry.signSkipper)) { doc.setFont('Helvetica', 'normal'); const skipperDate = formatPasskeySignDate(entry.signSkipper.signedAt); doc.text(`Passkey: ${entry.signSkipper.username}`, sigX + 2, sigY + 9); doc.text(skipperDate, sigX + 2, sigY + 13.5); } else if (isSignatureImage(entry.signSkipper)) { doc.addImage(entry.signSkipper, 'PNG', sigX + 2, sigY + 6, 72, 14) } else { doc.setFont('Helvetica', 'normal'); doc.text(String(entry.signSkipper || '—').toUpperCase(), sigX + 2, sigY + 11.2); } doc.setFont('Helvetica', 'bold'); doc.text('Crew Unterschrift:', sigX + 80.5, sigY + 4.2); if (isPasskeySignature(entry.signCrew)) { doc.setFont('Helvetica', 'normal'); const crewDate = formatPasskeySignDate(entry.signCrew.signedAt); doc.text(`Passkey: ${entry.signCrew.username}`, sigX + 80.5, sigY + 9); doc.text(crewDate, sigX + 80.5, sigY + 13.5); } else if (isClassicSignature(entry.signCrew)) { doc.setFont('Helvetica', 'normal'); const crewDate = formatPasskeySignDate(entry.signCrew.signedAt); doc.text(entry.signCrew.username, sigX + 80.5, sigY + 9); doc.text(crewDate, sigX + 80.5, sigY + 13.5); if (isSignatureImage(entry.signCrew.payload)) { doc.addImage(entry.signCrew.payload, 'PNG', sigX + 80.5, sigY + 6, 72, 14) } } else if (isSignatureImage(getSignaturePayload(entry.signCrew))) { doc.addImage(getSignaturePayload(entry.signCrew), 'PNG', sigX + 80.5, sigY + 6, 72, 14) } else { doc.setFont('Helvetica', 'normal'); doc.text(String(entry.signCrew || '—').toUpperCase(), sigX + 80.5, sigY + 11.2); } return doc; } export async function downloadLogbookPagePdf(logbookId: string, entryId: string, dateStr: string, preloadedData?: { yacht: any; entry: any }): Promise { const doc = await generateLogbookPagePdf(logbookId, entryId, preloadedData); const filename = `logbook_${dateStr.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.pdf`; doc.save(filename); }