import React, { useState, useEffect, useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import { db } from '../services/db.js' import { getActiveMasterKey } from '../services/auth.js' import { getLogbookKey } from '../services/logbookKeys.js' import { decryptJson, encryptJson } from '../services/crypto.js' import { syncLogbook } from '../services/sync.js' import { downloadCsv, shareCsv } from '../services/csvExport.js' import { downloadLogbookPagePdf } from '../services/pdfExport.js' import LogEntryEditor from './LogEntryEditor.tsx' import { useDialog } from './ModalDialog.tsx' import { FileText, Plus, Trash2, ChevronRight, Calendar, Download, Share2 } from 'lucide-react' import { carryOverTankLevelsFromPreviousDay, compareTravelDaysChronological, emptyTankLevels, formatTankLiters, getNextTravelDayNumber, type LogEntryTankSource, type TravelDaySortable } from '../utils/logEntryTankLevels.js' interface LogEntriesListProps { logbookId: string readOnly?: boolean preloadedYacht?: any preloadedEntries?: any[] preloadedPhotos?: any[] preloadedGpsTracks?: any[] controlledSelectedEntryId?: string | null onSelectedEntryIdChange?: (id: string | null) => void highlightEntryId?: string | null } interface DecryptedEntryItem { id: string date: string dayOfTravel: string departure: string destination: string updatedAt: string } export default function LogEntriesList({ logbookId, readOnly = false, preloadedYacht, preloadedEntries, preloadedPhotos, preloadedGpsTracks, controlledSelectedEntryId, onSelectedEntryIdChange, highlightEntryId }: LogEntriesListProps) { const { t } = useTranslation() const { showConfirm } = useDialog() const [entries, setEntries] = useState([]) const [internalSelectedEntryId, setInternalSelectedEntryId] = useState(null) const isEntrySelectionControlled = onSelectedEntryIdChange !== undefined const selectedEntryId = isEntrySelectionControlled ? (controlledSelectedEntryId ?? null) : internalSelectedEntryId const setSelectedEntryId = (entryId: string | null) => { if (isEntrySelectionControlled) { onSelectedEntryIdChange?.(entryId) } else { setInternalSelectedEntryId(entryId) } } const [loading, setLoading] = useState(false) const [exporting, setExporting] = useState(false) const [error, setError] = useState(null) const prevSelectedEntryIdRef = useRef(undefined) const loadEntries = useCallback(async () => { setLoading(true) setError(null) try { if (readOnly && preloadedEntries) { const list = preloadedEntries.map((entry: any) => ({ id: entry.payloadId || entry.id, date: entry.date || '', dayOfTravel: entry.dayOfTravel || '', departure: entry.departure || '', destination: entry.destination || '', updatedAt: entry.updatedAt || new Date().toISOString() })) list.sort((a, b) => { const dateCompare = new Date(b.date).getTime() - new Date(a.date).getTime() if (dateCompare !== 0) return dateCompare return Number(b.dayOfTravel) - Number(a.dayOfTravel) }) setEntries(list) return } const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') const local = await db.entries.where({ logbookId }).toArray() const list: DecryptedEntryItem[] = [] for (const entry of local) { const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey) if (decrypted) { list.push({ id: entry.payloadId, date: decrypted.date || '', dayOfTravel: decrypted.dayOfTravel || '', departure: decrypted.departure || '', destination: decrypted.destination || '', updatedAt: entry.updatedAt }) } } // Sort chronological descending (by date, or dayOfTravel numerical) list.sort((a, b) => { const dateCompare = new Date(b.date).getTime() - new Date(a.date).getTime() if (dateCompare !== 0) return dateCompare return Number(b.dayOfTravel) - Number(a.dayOfTravel) }) setEntries(list) } catch (err: any) { console.error('Failed to load log entries:', err) setError(err.message || 'Decryption failed. Could not load journal list.') } finally { setLoading(false) } }, [logbookId, readOnly, preloadedEntries]) useEffect(() => { loadEntries() }, [loadEntries]) useEffect(() => { const prevSelectedEntryId = prevSelectedEntryIdRef.current prevSelectedEntryIdRef.current = selectedEntryId if (prevSelectedEntryId !== undefined && prevSelectedEntryId !== null && selectedEntryId === null) { loadEntries() } }, [selectedEntryId, loadEntries]) const handleDownloadCsv = async () => { setExporting(true) setError(null) try { const title = preloadedYacht?.name || localStorage.getItem('active_logbook_title') || 'Logbook' if (readOnly && preloadedEntries && preloadedYacht) { await downloadCsv(logbookId, title, { yacht: preloadedYacht, entries: preloadedEntries }) } else { await downloadCsv(logbookId, title) } } catch (err: any) { console.error('Failed to download CSV:', err) setError(err.message || 'Failed to generate CSV export.') } finally { setExporting(false) } } const handleShareCsv = async () => { setExporting(true) setError(null) try { const title = preloadedYacht?.name || localStorage.getItem('active_logbook_title') || 'Logbook' if (readOnly && preloadedEntries && preloadedYacht) { await shareCsv(logbookId, title, { yacht: preloadedYacht, entries: preloadedEntries }) } else { await shareCsv(logbookId, title) } } catch (err: any) { if (err.message === 'share_unsupported') { const title = preloadedYacht?.name || localStorage.getItem('active_logbook_title') || 'Logbook' if (readOnly && preloadedEntries && preloadedYacht) { await downloadCsv(logbookId, title, { yacht: preloadedYacht, entries: preloadedEntries }) } else { await downloadCsv(logbookId, title) } setError(t('logs.share_unsupported')) } else { console.error('Failed to share CSV:', err) setError(err.message || 'Failed to share CSV export.') } } finally { setExporting(false) } } const handleDownloadPdf = async (entryId: string, date: string, e: React.MouseEvent) => { e.stopPropagation() setExporting(true) setError(null) try { if (readOnly && preloadedEntries && preloadedYacht) { const fullEntry = preloadedEntries.find(entry => (entry.payloadId || entry.id) === entryId) await downloadLogbookPagePdf(logbookId, entryId, date, { yacht: preloadedYacht, entry: fullEntry }) } else { await downloadLogbookPagePdf(logbookId, entryId, date) } } catch (err: any) { console.error('Failed to download PDF:', err) setError(err.message || 'Failed to generate PDF export.') } finally { setExporting(false) } } const handleCreate = async () => { if (readOnly) return setError(null) try { const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') const localEntries = await db.entries.where({ logbookId }).toArray() const decryptedEntries: Array = [] for (const entry of localEntries) { const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey) if (decrypted) decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable) } decryptedEntries.sort(compareTravelDaysChronological) const previousEntry = decryptedEntries.at(-1) ?? null let { freshwater, fuel } = carryOverTankLevelsFromPreviousDay(previousEntry) if (previousEntry && (freshwater.morning > 0 || fuel.morning > 0)) { const confirmed = await showConfirm( t('logs.carry_over_tanks_confirm', { fw: formatTankLiters(freshwater.morning), fuel: formatTankLiters(fuel.morning) }), t('logs.carry_over_tanks_title'), t('logs.carry_over_tanks_yes'), t('logs.carry_over_tanks_no') ) if (!confirmed) { freshwater = emptyTankLevels() fuel = emptyTankLevels() } } setLoading(true) const localId = window.crypto.randomUUID() const nowStr = new Date().toISOString() const todayStr = nowStr.substring(0, 10) const initialPayload = { date: todayStr, dayOfTravel: getNextTravelDayNumber(decryptedEntries), departure: '', destination: '', freshwater, fuel, signSkipper: '', signCrew: '', events: [] } const encrypted = await encryptJson(initialPayload, masterKey) // Save locally await db.entries.put({ payloadId: localId, logbookId, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: nowStr }) // Queue for background sync await db.syncQueue.put({ action: 'create', type: 'entry', payloadId: localId, logbookId, data: JSON.stringify(encrypted), updatedAt: nowStr }) // Open immediately in details editor setSelectedEntryId(localId) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } catch (err: any) { console.error('Failed to create entry:', err) setError(err.message || 'Failed to create new log entry.') } finally { setLoading(false) } } const handleDelete = async (entryId: string, e: React.MouseEvent) => { e.stopPropagation() if (readOnly) return if (await showConfirm(t('logs.delete_confirm'), t('logs.delete_entry'), t('logs.confirm_yes'), t('logs.confirm_no'))) { setError(null) try { const now = new Date().toISOString() await db.entries.delete(entryId) await db.syncQueue.put({ action: 'delete', type: 'entry', payloadId: entryId, logbookId, data: '', updatedAt: now }) setEntries((prev) => prev.filter((item) => item.id !== entryId)) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } catch (err: any) { console.error('Failed to delete log entry:', err) setError(err.message || 'Failed to delete log entry.') } } } if (selectedEntryId) { return ( setSelectedEntryId(null)} readOnly={readOnly} preloadedEntry={preloadedEntries?.find(entry => (entry.payloadId || entry.id) === selectedEntryId)} preloadedPhotos={preloadedPhotos} preloadedTrack={preloadedGpsTracks?.find(track => track.entryId === selectedEntryId)} /> ) } if (loading) { return (

{t('logs.loading')}

) } return (

{t('logs.title')}

{!readOnly && ( )}
{error &&
{error}
} {entries.length === 0 ? (
{t('logs.no_entries')}
) : (
{entries.map((item) => (
setSelectedEntryId(item.id)} >

{item.departure && item.destination ? `${item.departure} → ${item.destination}` : t('logs.new_entry')}

{t('logs.day_of_travel')} {item.dayOfTravel} {new Date(item.date).toLocaleDateString()}
{!readOnly && ( )}
))}
)}
) }