|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
|
|
|
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
|
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
|
import { db } from '../services/db.js'
|
|
|
|
|
import { getActiveMasterKey } from '../services/auth.js'
|
|
|
|
@@ -6,7 +6,7 @@ import { getLogbookKey } from '../services/logbookKeys.js'
|
|
|
|
|
import { encryptJson, decryptJson } from '../services/crypto.js'
|
|
|
|
|
import { syncLogbook } from '../services/sync.js'
|
|
|
|
|
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
|
|
|
|
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload } from 'lucide-react'
|
|
|
|
|
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload, Pencil, X } from 'lucide-react'
|
|
|
|
|
import PhotoCapture from './PhotoCapture.tsx'
|
|
|
|
|
import SignatureSection from './SignatureSection.tsx'
|
|
|
|
|
import TrackMap from './TrackMap.tsx'
|
|
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
|
|
hasAnySignature
|
|
|
|
|
} from '../utils/signatures.js'
|
|
|
|
|
import type { SignatureValue } from '../types/signatures.js'
|
|
|
|
|
import { buildLogEntryPayload } from '../utils/logEntryPayload.js'
|
|
|
|
|
import { buildLogEntryPayload, type LogEventPayload } from '../utils/logEntryPayload.js'
|
|
|
|
|
import { hashEntryForSigning } from '../utils/entryCanonicalHash.js'
|
|
|
|
|
import { signLogEntry } from '../services/entrySigning.js'
|
|
|
|
|
import { getLogbookAccess } from '../services/logbookAccess.js'
|
|
|
|
@@ -34,6 +34,56 @@ import {
|
|
|
|
|
} from '../services/trackUpload.js'
|
|
|
|
|
import { computeTrackStats, formatTrackStats } from '../utils/trackStats.js'
|
|
|
|
|
|
|
|
|
|
function emptyTankLevels() {
|
|
|
|
|
return { morning: 0, refilled: 0, evening: 0, consumption: 0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fingerprintFromStoredEntry(decrypted: Record<string, unknown>): string {
|
|
|
|
|
const fw = (decrypted.freshwater as Record<string, number> | undefined) ?? emptyTankLevels()
|
|
|
|
|
const fuel = (decrypted.fuel as Record<string, number> | undefined) ?? emptyTankLevels()
|
|
|
|
|
const trackDistance = decrypted.trackDistanceNm
|
|
|
|
|
const trackSpeedMax = decrypted.trackSpeedMaxKn
|
|
|
|
|
const trackSpeedAvg = decrypted.trackSpeedAvgKn
|
|
|
|
|
|
|
|
|
|
const payload = buildLogEntryPayload({
|
|
|
|
|
date: String(decrypted.date || ''),
|
|
|
|
|
dayOfTravel: String(decrypted.dayOfTravel || ''),
|
|
|
|
|
departure: String(decrypted.departure || ''),
|
|
|
|
|
destination: String(decrypted.destination || ''),
|
|
|
|
|
freshwater: {
|
|
|
|
|
morning: fw.morning || 0,
|
|
|
|
|
refilled: fw.refilled || 0,
|
|
|
|
|
evening: fw.evening || 0,
|
|
|
|
|
consumption: fw.consumption ?? 0
|
|
|
|
|
},
|
|
|
|
|
fuel: {
|
|
|
|
|
morning: fuel.morning || 0,
|
|
|
|
|
refilled: fuel.refilled || 0,
|
|
|
|
|
evening: fuel.evening || 0,
|
|
|
|
|
consumption: fuel.consumption ?? 0
|
|
|
|
|
},
|
|
|
|
|
trackDistanceNm:
|
|
|
|
|
trackDistance != null && trackDistance !== ''
|
|
|
|
|
? parseFloat(String(trackDistance))
|
|
|
|
|
: undefined,
|
|
|
|
|
trackSpeedMaxKn:
|
|
|
|
|
trackSpeedMax != null && trackSpeedMax !== ''
|
|
|
|
|
? parseFloat(String(trackSpeedMax))
|
|
|
|
|
: undefined,
|
|
|
|
|
trackSpeedAvgKn:
|
|
|
|
|
trackSpeedAvg != null && trackSpeedAvg !== ''
|
|
|
|
|
? parseFloat(String(trackSpeedAvg))
|
|
|
|
|
: undefined,
|
|
|
|
|
events: (decrypted.events as LogEventPayload[]) || []
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return JSON.stringify({
|
|
|
|
|
...payload,
|
|
|
|
|
signSkipper: serializeSignature(normalizeSignature(decrypted.signSkipper as SignatureValue | '') || '') ?? '',
|
|
|
|
|
signCrew: serializeSignature(normalizeSignature(decrypted.signCrew as SignatureValue | '') || '') ?? ''
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LogEntryEditorProps {
|
|
|
|
|
entryId: string
|
|
|
|
|
logbookId: string
|
|
|
|
@@ -45,24 +95,7 @@ interface LogEntryEditorProps {
|
|
|
|
|
preloadedYacht?: any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LogEvent {
|
|
|
|
|
time: string
|
|
|
|
|
mgk: string
|
|
|
|
|
rwk: string
|
|
|
|
|
windPressure: string
|
|
|
|
|
windDirection: string
|
|
|
|
|
windStrength: string
|
|
|
|
|
seaState: string
|
|
|
|
|
weatherIcon: string
|
|
|
|
|
current: string
|
|
|
|
|
heel: string
|
|
|
|
|
sailsOrMotor: string
|
|
|
|
|
logReading: string
|
|
|
|
|
distance: string
|
|
|
|
|
gpsLat: string
|
|
|
|
|
gpsLng: string
|
|
|
|
|
remarks: string
|
|
|
|
|
}
|
|
|
|
|
interface LogEvent extends LogEventPayload {}
|
|
|
|
|
|
|
|
|
|
export default function LogEntryEditor({
|
|
|
|
|
entryId,
|
|
|
|
@@ -139,6 +172,7 @@ export default function LogEntryEditor({
|
|
|
|
|
const [success, setSuccess] = useState(false)
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
const [weatherLoading, setWeatherLoading] = useState(false)
|
|
|
|
|
const [savedFingerprint, setSavedFingerprint] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
// Track file upload
|
|
|
|
|
const [savedTrack, setSavedTrack] = useState<SavedTrack | null>(null)
|
|
|
|
@@ -148,6 +182,8 @@ export default function LogEntryEditor({
|
|
|
|
|
const lockedContentHashRef = useRef<string | null>(null)
|
|
|
|
|
const contentReadyRef = useRef(false)
|
|
|
|
|
const lastSignatureAlertHashRef = useRef<string | null>(null)
|
|
|
|
|
const skipCrewSignClearRef = useRef(false)
|
|
|
|
|
const [editingEventIndex, setEditingEventIndex] = useState<number | null>(null)
|
|
|
|
|
|
|
|
|
|
const applyTrackStats = (waypoints: SavedTrack['waypoints']) => {
|
|
|
|
|
const stats = computeTrackStats(waypoints)
|
|
|
|
@@ -170,7 +206,7 @@ export default function LogEntryEditor({
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const buildPayloadForSigning = useCallback(() => {
|
|
|
|
|
const buildPayloadForSigning = useCallback((eventsOverride?: LogEvent[]) => {
|
|
|
|
|
return buildLogEntryPayload({
|
|
|
|
|
date,
|
|
|
|
|
dayOfTravel,
|
|
|
|
@@ -191,7 +227,7 @@ export default function LogEntryEditor({
|
|
|
|
|
trackDistanceNm: trackDistanceNm.trim() ? parseFloat(trackDistanceNm) : undefined,
|
|
|
|
|
trackSpeedMaxKn: trackSpeedMaxKn.trim() ? parseFloat(trackSpeedMaxKn) : undefined,
|
|
|
|
|
trackSpeedAvgKn: trackSpeedAvgKn.trim() ? parseFloat(trackSpeedAvgKn) : undefined,
|
|
|
|
|
events
|
|
|
|
|
events: eventsOverride ?? events
|
|
|
|
|
})
|
|
|
|
|
}, [
|
|
|
|
|
date, dayOfTravel, departure, destination,
|
|
|
|
@@ -201,6 +237,61 @@ export default function LogEntryEditor({
|
|
|
|
|
events
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const currentFingerprint = useMemo(() => {
|
|
|
|
|
const payload = buildPayloadForSigning()
|
|
|
|
|
return JSON.stringify({
|
|
|
|
|
...payload,
|
|
|
|
|
signSkipper: serializeSignature(signSkipper) ?? '',
|
|
|
|
|
signCrew: serializeSignature(signCrew) ?? ''
|
|
|
|
|
})
|
|
|
|
|
}, [buildPayloadForSigning, signSkipper, signCrew])
|
|
|
|
|
|
|
|
|
|
const isDirty = savedFingerprint !== null && currentFingerprint !== savedFingerprint
|
|
|
|
|
|
|
|
|
|
const persistEntryToDb = useCallback(async (eventsOverride?: LogEvent[]) => {
|
|
|
|
|
if (readOnly) return
|
|
|
|
|
|
|
|
|
|
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
|
|
|
|
|
if (!masterKey) throw new Error('Encryption key not found. Please log in.')
|
|
|
|
|
|
|
|
|
|
const entryData = {
|
|
|
|
|
...buildPayloadForSigning(eventsOverride),
|
|
|
|
|
signSkipper: serializeSignature(signSkipper),
|
|
|
|
|
signCrew: serializeSignature(signCrew)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const encrypted = await encryptJson(entryData, masterKey)
|
|
|
|
|
const now = new Date().toISOString()
|
|
|
|
|
|
|
|
|
|
await db.entries.put({
|
|
|
|
|
payloadId: entryId,
|
|
|
|
|
logbookId,
|
|
|
|
|
encryptedData: encrypted.ciphertext,
|
|
|
|
|
iv: encrypted.iv,
|
|
|
|
|
tag: encrypted.tag,
|
|
|
|
|
updatedAt: now
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await db.syncQueue.put({
|
|
|
|
|
action: 'update',
|
|
|
|
|
type: 'entry',
|
|
|
|
|
payloadId: entryId,
|
|
|
|
|
logbookId,
|
|
|
|
|
data: JSON.stringify(encrypted),
|
|
|
|
|
updatedAt: now
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err))
|
|
|
|
|
|
|
|
|
|
setSavedFingerprint(JSON.stringify({
|
|
|
|
|
...buildPayloadForSigning(eventsOverride),
|
|
|
|
|
signSkipper: serializeSignature(signSkipper) ?? '',
|
|
|
|
|
signCrew: serializeSignature(signCrew) ?? ''
|
|
|
|
|
}))
|
|
|
|
|
}, [
|
|
|
|
|
readOnly, logbookId, entryId, events, buildPayloadForSigning, signSkipper, signCrew
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleOnline = () => setIsOnline(true)
|
|
|
|
|
const handleOffline = () => setIsOnline(false)
|
|
|
|
@@ -253,13 +344,17 @@ export default function LogEntryEditor({
|
|
|
|
|
|
|
|
|
|
if (entryHash !== lockedContentHashRef.current) {
|
|
|
|
|
lockedContentHashRef.current = null
|
|
|
|
|
setSignSkipper('')
|
|
|
|
|
setSignCrew('')
|
|
|
|
|
if (lastSignatureAlertHashRef.current !== entryHash) {
|
|
|
|
|
const hadSkipper = !!signSkipper
|
|
|
|
|
const hadCrew = !!signCrew
|
|
|
|
|
const skipperOnly = skipCrewSignClearRef.current
|
|
|
|
|
skipCrewSignClearRef.current = false
|
|
|
|
|
if (hadSkipper) setSignSkipper('')
|
|
|
|
|
if (hadCrew && !skipperOnly) setSignCrew('')
|
|
|
|
|
if (lastSignatureAlertHashRef.current !== entryHash && (hadSkipper || (hadCrew && !skipperOnly))) {
|
|
|
|
|
lastSignatureAlertHashRef.current = entryHash
|
|
|
|
|
void showAlertRef.current(
|
|
|
|
|
t('logs.sign_cleared_re_sign'),
|
|
|
|
|
t('logs.sign_cleared_re_sign_title')
|
|
|
|
|
skipperOnly ? t('logs.sign_cleared_skipper_re_sign') : t('logs.sign_cleared_re_sign'),
|
|
|
|
|
skipperOnly ? t('logs.sign_cleared_skipper_re_sign_title') : t('logs.sign_cleared_re_sign_title')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -359,6 +454,7 @@ export default function LogEntryEditor({
|
|
|
|
|
async function loadEntry() {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
setError(null)
|
|
|
|
|
setSavedFingerprint(null)
|
|
|
|
|
lockedContentHashRef.current = null
|
|
|
|
|
contentReadyRef.current = false
|
|
|
|
|
lastSignatureAlertHashRef.current = null
|
|
|
|
@@ -386,6 +482,7 @@ export default function LogEntryEditor({
|
|
|
|
|
setSignCrew(normalizeSignature(preloadedEntry.signCrew) || '')
|
|
|
|
|
loadTrackStatsFromEntry(preloadedEntry)
|
|
|
|
|
setEvents(preloadedEntry.events || [])
|
|
|
|
|
setSavedFingerprint(fingerprintFromStoredEntry(preloadedEntry))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -418,6 +515,7 @@ export default function LogEntryEditor({
|
|
|
|
|
setSignCrew(normalizeSignature(decrypted.signCrew) || '')
|
|
|
|
|
loadTrackStatsFromEntry(decrypted)
|
|
|
|
|
setEvents(decrypted.events || [])
|
|
|
|
|
setSavedFingerprint(fingerprintFromStoredEntry(decrypted))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
@@ -692,32 +790,26 @@ export default function LogEntryEditor({
|
|
|
|
|
return currentItems.includes(item.toLowerCase())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleAddEvent = (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (readOnly || !evTime) return
|
|
|
|
|
const buildEventFromForm = (): LogEvent => ({
|
|
|
|
|
time: evTime,
|
|
|
|
|
mgk: evMgk.trim(),
|
|
|
|
|
rwk: evRwk.trim(),
|
|
|
|
|
windPressure: evWindPressure.trim(),
|
|
|
|
|
windDirection: evWindDirection.trim(),
|
|
|
|
|
windStrength: evWindStrength.trim(),
|
|
|
|
|
seaState: evSeaState.trim(),
|
|
|
|
|
weatherIcon: evWeatherIcon.trim(),
|
|
|
|
|
current: evCurrent.trim(),
|
|
|
|
|
heel: evHeel.trim(),
|
|
|
|
|
sailsOrMotor: evSailsOrMotor.trim(),
|
|
|
|
|
logReading: evLogReading.trim(),
|
|
|
|
|
distance: evDistance.trim(),
|
|
|
|
|
gpsLat: evGpsLat.trim(),
|
|
|
|
|
gpsLng: evGpsLng.trim(),
|
|
|
|
|
remarks: evRemarks.trim()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const newEvent: LogEvent = {
|
|
|
|
|
time: evTime,
|
|
|
|
|
mgk: evMgk.trim(),
|
|
|
|
|
rwk: evRwk.trim(),
|
|
|
|
|
windPressure: evWindPressure.trim(),
|
|
|
|
|
windDirection: evWindDirection.trim(),
|
|
|
|
|
windStrength: evWindStrength.trim(),
|
|
|
|
|
seaState: evSeaState.trim(),
|
|
|
|
|
weatherIcon: evWeatherIcon.trim(),
|
|
|
|
|
current: evCurrent.trim(),
|
|
|
|
|
heel: evHeel.trim(),
|
|
|
|
|
sailsOrMotor: evSailsOrMotor.trim(),
|
|
|
|
|
logReading: evLogReading.trim(),
|
|
|
|
|
distance: evDistance.trim(),
|
|
|
|
|
gpsLat: evGpsLat.trim(),
|
|
|
|
|
gpsLng: evGpsLng.trim(),
|
|
|
|
|
remarks: evRemarks.trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setEvents((prev) => [...prev, newEvent])
|
|
|
|
|
|
|
|
|
|
// Clear event form fields
|
|
|
|
|
const clearEventForm = () => {
|
|
|
|
|
setEvTime('')
|
|
|
|
|
setEvMgk('')
|
|
|
|
|
setEvRwk('')
|
|
|
|
@@ -735,11 +827,103 @@ export default function LogEntryEditor({
|
|
|
|
|
setEvGpsLng('')
|
|
|
|
|
setEvRemarks('')
|
|
|
|
|
setEvLocationName('')
|
|
|
|
|
setEditingEventIndex(null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeleteEvent = (index: number) => {
|
|
|
|
|
const fillEventForm = (ev: LogEvent) => {
|
|
|
|
|
setEvTime(ev.time)
|
|
|
|
|
setEvMgk(ev.mgk)
|
|
|
|
|
setEvRwk(ev.rwk)
|
|
|
|
|
setEvWindPressure(ev.windPressure)
|
|
|
|
|
setEvWindDirection(ev.windDirection)
|
|
|
|
|
setEvWindStrength(ev.windStrength)
|
|
|
|
|
setEvSeaState(ev.seaState)
|
|
|
|
|
setEvWeatherIcon(ev.weatherIcon)
|
|
|
|
|
setEvCurrent(ev.current)
|
|
|
|
|
setEvHeel(ev.heel)
|
|
|
|
|
setEvSailsOrMotor(ev.sailsOrMotor)
|
|
|
|
|
setEvLogReading(ev.logReading)
|
|
|
|
|
setEvDistance(ev.distance)
|
|
|
|
|
setEvGpsLat(ev.gpsLat)
|
|
|
|
|
setEvGpsLng(ev.gpsLng)
|
|
|
|
|
setEvRemarks(ev.remarks)
|
|
|
|
|
setEvLocationName('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const markSkipperSignatureClearedForEventChange = () => {
|
|
|
|
|
if (!signSkipper) return
|
|
|
|
|
skipCrewSignClearRef.current = true
|
|
|
|
|
setSignSkipper('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleEditEvent = (index: number) => {
|
|
|
|
|
if (readOnly) return
|
|
|
|
|
setEvents((prev) => prev.filter((_, idx) => idx !== index))
|
|
|
|
|
const ev = events[index]
|
|
|
|
|
if (!ev) return
|
|
|
|
|
fillEventForm(ev)
|
|
|
|
|
setEditingEventIndex(index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCancelEventEdit = () => {
|
|
|
|
|
clearEventForm()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSaveEvent = async (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (readOnly || !evTime) return
|
|
|
|
|
|
|
|
|
|
const eventData = buildEventFromForm()
|
|
|
|
|
let nextEvents: LogEvent[]
|
|
|
|
|
|
|
|
|
|
if (editingEventIndex !== null) {
|
|
|
|
|
const hadSkipperSignature = !!signSkipper
|
|
|
|
|
markSkipperSignatureClearedForEventChange()
|
|
|
|
|
nextEvents = events.map((ev, idx) => (idx === editingEventIndex ? eventData : ev))
|
|
|
|
|
if (hadSkipperSignature) {
|
|
|
|
|
void showAlertRef.current(
|
|
|
|
|
t('logs.sign_cleared_skipper_re_sign'),
|
|
|
|
|
t('logs.sign_cleared_skipper_re_sign_title')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
nextEvents = [...events, eventData]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setEvents(nextEvents)
|
|
|
|
|
clearEventForm()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await persistEntryToDb(nextEvents)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Failed to auto-save event:', err)
|
|
|
|
|
setError(err.message || 'Failed to save event.')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeleteEvent = async (index: number) => {
|
|
|
|
|
if (readOnly) return
|
|
|
|
|
const hadSkipperSignature = !!signSkipper
|
|
|
|
|
markSkipperSignatureClearedForEventChange()
|
|
|
|
|
const nextEvents = events.filter((_, idx) => idx !== index)
|
|
|
|
|
setEvents(nextEvents)
|
|
|
|
|
if (hadSkipperSignature) {
|
|
|
|
|
void showAlertRef.current(
|
|
|
|
|
t('logs.sign_cleared_skipper_re_sign'),
|
|
|
|
|
t('logs.sign_cleared_skipper_re_sign_title')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (editingEventIndex === index) {
|
|
|
|
|
clearEventForm()
|
|
|
|
|
} else if (editingEventIndex !== null && index < editingEventIndex) {
|
|
|
|
|
setEditingEventIndex(editingEventIndex - 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await persistEntryToDb(nextEvents)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Failed to auto-save after event delete:', err)
|
|
|
|
|
setError(err.message || 'Failed to save event deletion.')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDownloadPdf = async () => {
|
|
|
|
@@ -758,45 +942,13 @@ export default function LogEntryEditor({
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (readOnly) return
|
|
|
|
|
if (readOnly || !isDirty) return
|
|
|
|
|
setSaving(true)
|
|
|
|
|
setError(null)
|
|
|
|
|
setSuccess(false)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
|
|
|
|
|
if (!masterKey) throw new Error('Encryption key not found. Please log in.')
|
|
|
|
|
|
|
|
|
|
const entryPayload = buildPayloadForSigning()
|
|
|
|
|
const entryData = {
|
|
|
|
|
...entryPayload,
|
|
|
|
|
signSkipper: serializeSignature(signSkipper),
|
|
|
|
|
signCrew: serializeSignature(signCrew)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// E2E encrypt
|
|
|
|
|
const encrypted = await encryptJson(entryData, masterKey)
|
|
|
|
|
const now = new Date().toISOString()
|
|
|
|
|
|
|
|
|
|
// Save locally
|
|
|
|
|
await db.entries.put({
|
|
|
|
|
payloadId: entryId,
|
|
|
|
|
logbookId,
|
|
|
|
|
encryptedData: encrypted.ciphertext,
|
|
|
|
|
iv: encrypted.iv,
|
|
|
|
|
tag: encrypted.tag,
|
|
|
|
|
updatedAt: now
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Queue for background sync
|
|
|
|
|
await db.syncQueue.put({
|
|
|
|
|
action: 'update',
|
|
|
|
|
type: 'entry',
|
|
|
|
|
payloadId: entryId,
|
|
|
|
|
logbookId,
|
|
|
|
|
data: JSON.stringify(encrypted),
|
|
|
|
|
updatedAt: now
|
|
|
|
|
})
|
|
|
|
|
await persistEntryToDb()
|
|
|
|
|
|
|
|
|
|
setSuccess(true)
|
|
|
|
|
trackPlausibleEvent(PlausibleEvents.TRAVEL_DAY_SAVED)
|
|
|
|
@@ -804,8 +956,6 @@ export default function LogEntryEditor({
|
|
|
|
|
setSuccess(false)
|
|
|
|
|
onBack()
|
|
|
|
|
}, 1500)
|
|
|
|
|
|
|
|
|
|
syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err))
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Failed to save entry details:', err)
|
|
|
|
|
setError(err.message || 'Failed to save entry details.')
|
|
|
|
@@ -1079,8 +1229,22 @@ export default function LogEntryEditor({
|
|
|
|
|
</td>
|
|
|
|
|
<td className="remarks-td">{ev.remarks}</td>
|
|
|
|
|
{!readOnly && (
|
|
|
|
|
<td>
|
|
|
|
|
<button type="button" className="btn-icon logout" onClick={() => handleDeleteEvent(idx)}>
|
|
|
|
|
<td className="events-actions-td">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-icon"
|
|
|
|
|
onClick={() => handleEditEvent(idx)}
|
|
|
|
|
title={t('logs.edit_event')}
|
|
|
|
|
disabled={editingEventIndex !== null && editingEventIndex !== idx}
|
|
|
|
|
>
|
|
|
|
|
<Pencil size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-icon logout"
|
|
|
|
|
onClick={() => handleDeleteEvent(idx)}
|
|
|
|
|
title={t('logs.delete_event')}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</td>
|
|
|
|
@@ -1095,7 +1259,9 @@ export default function LogEntryEditor({
|
|
|
|
|
{/* Add New Event Form Sub-Card */}
|
|
|
|
|
{!readOnly && (
|
|
|
|
|
<div className="member-editor-card glass">
|
|
|
|
|
<h4 style={{ margin: '0 0 16px 0', color: '#fbbf24' }}>{t('logs.add_event')}</h4>
|
|
|
|
|
<h4 style={{ margin: '0 0 16px 0', color: '#fbbf24' }}>
|
|
|
|
|
{editingEventIndex !== null ? t('logs.edit_event') : t('logs.add_event')}
|
|
|
|
|
</h4>
|
|
|
|
|
|
|
|
|
|
<div className="form-grid mb-4">
|
|
|
|
|
<div className="input-group">
|
|
|
|
@@ -1329,16 +1495,30 @@ export default function LogEntryEditor({
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn secondary"
|
|
|
|
|
onClick={handleAddEvent}
|
|
|
|
|
disabled={saving || !evTime}
|
|
|
|
|
style={{ width: 'auto', padding: '10px 20px', marginLeft: 'auto', display: 'flex' }}
|
|
|
|
|
>
|
|
|
|
|
<Plus size={16} />
|
|
|
|
|
{t('logs.add_event_btn')}
|
|
|
|
|
</button>
|
|
|
|
|
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto', flexWrap: 'wrap' }}>
|
|
|
|
|
{editingEventIndex !== null && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn secondary"
|
|
|
|
|
onClick={handleCancelEventEdit}
|
|
|
|
|
disabled={saving}
|
|
|
|
|
style={{ width: 'auto', padding: '10px 20px', display: 'flex' }}
|
|
|
|
|
>
|
|
|
|
|
<X size={16} />
|
|
|
|
|
{t('logs.cancel_event_edit')}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn secondary"
|
|
|
|
|
onClick={handleSaveEvent}
|
|
|
|
|
disabled={saving || !evTime}
|
|
|
|
|
style={{ width: 'auto', padding: '10px 20px', display: 'flex' }}
|
|
|
|
|
>
|
|
|
|
|
{editingEventIndex !== null ? <Save size={16} /> : <Plus size={16} />}
|
|
|
|
|
{editingEventIndex !== null ? t('logs.save_event_btn') : t('logs.add_event_btn')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
@@ -1495,7 +1675,7 @@ export default function LogEntryEditor({
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<button type="submit" className="btn primary" disabled={saving || !date || !dayOfTravel.trim()}>
|
|
|
|
|
<button type="submit" className="btn primary" disabled={saving || !date || !dayOfTravel.trim() || !isDirty}>
|
|
|
|
|
<Save size={18} />
|
|
|
|
|
{saving ? t('logs.saving') : t('logs.save')}
|
|
|
|
|
</button>
|
|
|
|
|