diff --git a/client/src/App.css b/client/src/App.css index d24d756..565290b 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1564,6 +1564,7 @@ body:has(.theme-cupertino) { color: #e2e8f0; line-height: 1.5; margin: 0 0 24px 0; + white-space: pre-line; } .custom-dialog-actions { diff --git a/client/src/components/LogEntriesList.tsx b/client/src/components/LogEntriesList.tsx index 72e9b58..c4b4ce3 100644 --- a/client/src/components/LogEntriesList.tsx +++ b/client/src/components/LogEntriesList.tsx @@ -10,6 +10,15 @@ 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 @@ -179,26 +188,52 @@ export default function LogEntriesList({ const handleCreate = async () => { if (readOnly) return - setLoading(true) 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) - // Calculate next travel day number - const nextDayNum = String(entries.length + 1) - const initialPayload = { date: todayStr, - dayOfTravel: nextDayNum, + dayOfTravel: getNextTravelDayNumber(decryptedEntries), departure: '', destination: '', - freshwater: { morning: 0, refilled: 0, evening: 0, consumption: 0 }, - fuel: { morning: 0, refilled: 0, evening: 0, consumption: 0 }, + freshwater, + fuel, signSkipper: '', signCrew: '', events: [] diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index f5332b3..4f05224 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -125,6 +125,10 @@ "loading": "Journal wird geladen...", "delete_entry": "Tag löschen", "delete_confirm": "Sind Sie sicher, dass Sie diesen Reisetag unwiderruflich löschen möchten?", + "carry_over_tanks_title": "Tankstände übernehmen?", + "carry_over_tanks_confirm": "Morgenstände vom letzten Reisetag als Startwerte übernehmen?\n\nFrischwasser: {{fw}} L\nKraftstoff: {{fuel}} L", + "carry_over_tanks_yes": "Übernehmen", + "carry_over_tanks_no": "Mit 0 starten", "event_title": "Chronologisches Ereignisprotokoll", "no_events": "Noch keine Ereignisse für diesen Reisetag eingetragen.", "event_time": "Uhrzeit", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index 972edc7..9edfc84 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -125,6 +125,10 @@ "loading": "Loading journal...", "delete_entry": "Delete Day", "delete_confirm": "Are you sure you want to permanently delete this travel day?", + "carry_over_tanks_title": "Carry over tank levels?", + "carry_over_tanks_confirm": "Use the previous travel day's closing levels as morning levels?\n\nFreshwater: {{fw}} L\nFuel: {{fuel}} L", + "carry_over_tanks_yes": "Carry over", + "carry_over_tanks_no": "Start at 0", "event_title": "Chronological Event Logbook", "no_events": "No events logged for this travel day yet.", "event_time": "Time", diff --git a/client/src/utils/logEntryTankLevels.ts b/client/src/utils/logEntryTankLevels.ts new file mode 100644 index 0000000..c12fbe3 --- /dev/null +++ b/client/src/utils/logEntryTankLevels.ts @@ -0,0 +1,64 @@ +export interface TankLevels { + morning: number + refilled: number + evening: number + consumption: number +} + +export interface TravelDaySortable { + date?: string + dayOfTravel?: string | number +} + +/** Chronological order: date ascending, then day of travel ascending. */ +export function compareTravelDaysChronological(a: TravelDaySortable, b: TravelDaySortable): number { + const dateCompare = new Date(a.date || 0).getTime() - new Date(b.date || 0).getTime() + if (dateCompare !== 0) return dateCompare + return Number(a.dayOfTravel || 0) - Number(b.dayOfTravel || 0) +} + +export function getNextTravelDayNumber(entries: TravelDaySortable[]): string { + const maxDay = entries.reduce((max, entry) => Math.max(max, Number(entry.dayOfTravel) || 0), 0) + return String(maxDay + 1) +} + +/** Closing level at end of travel day: evening stand, else calculated balance, else morning. */ +export function getClosingTankLevel(tank?: Partial | null): number { + if (!tank) return 0 + + const evening = Number(tank.evening) || 0 + if (evening > 0) return evening + + const morning = Number(tank.morning) || 0 + const refilled = Number(tank.refilled) || 0 + const consumption = Number(tank.consumption) || 0 + const fromBalance = morning + refilled - consumption + if (fromBalance > 0) return fromBalance + + return morning +} + +export interface LogEntryTankSource { + freshwater?: Partial + fuel?: Partial +} + +export function emptyTankLevels(morning = 0): TankLevels { + return { morning, refilled: 0, evening: 0, consumption: 0 } +} + +export function formatTankLiters(liters: number): string { + if (!Number.isFinite(liters) || liters <= 0) return '0' + return Number.isInteger(liters) ? String(liters) : liters.toFixed(1) +} + +export function carryOverTankLevelsFromPreviousDay(previousEntry?: LogEntryTankSource | null): { freshwater: TankLevels; fuel: TankLevels } { + if (!previousEntry) { + return { freshwater: emptyTankLevels(), fuel: emptyTankLevels() } + } + + return { + freshwater: emptyTankLevels(getClosingTankLevel(previousEntry.freshwater)), + fuel: emptyTankLevels(getClosingTankLevel(previousEntry.fuel)) + } +}