From aa2b35ddace9cf3e2e8b72cc0771889390c7deaf Mon Sep 17 00:00:00 2001 From: elpatron Date: Sat, 30 May 2026 10:56:45 +0200 Subject: [PATCH] feat: Ereignisprotokoll bearbeiten und Skipper-Signatur invalidieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bestehende Ereigniszeilen lassen sich nachträglich ändern; beim Speichern oder Löschen wird nur die Skipper-Unterschrift entfernt, die Crew-Signatur bleibt. Co-authored-by: Cursor --- client/src/App.css | 12 ++ client/src/components/LogEntryEditor.tsx | 193 +++++++++++++++++------ client/src/i18n/locales/de.json | 6 + client/src/i18n/locales/en.json | 6 + 4 files changed, 173 insertions(+), 44 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 3552501..cc57c54 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1430,6 +1430,18 @@ html.scheme-dark .themed-select-option.is-selected { vertical-align: middle; } +.events-actions-td { + white-space: nowrap; +} + +.events-actions-td .btn-icon { + margin-left: 4px; +} + +.events-actions-td .btn-icon:first-child { + margin-left: 0; +} + .events-table tbody tr:hover { background: rgba(255, 255, 255, 0.02); } diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index 112c709..9132594 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -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' @@ -148,6 +148,8 @@ export default function LogEntryEditor({ const lockedContentHashRef = useRef(null) const contentReadyRef = useRef(false) const lastSignatureAlertHashRef = useRef(null) + const skipCrewSignClearRef = useRef(false) + const [editingEventIndex, setEditingEventIndex] = useState(null) const applyTrackStats = (waypoints: SavedTrack['waypoints']) => { const stats = computeTrackStats(waypoints) @@ -253,13 +255,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') ) } } @@ -692,32 +698,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 +735,86 @@ export default function LogEntryEditor({ setEvGpsLng('') setEvRemarks('') setEvLocationName('') + setEditingEventIndex(null) + } + + 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 + const ev = events[index] + if (!ev) return + fillEventForm(ev) + setEditingEventIndex(index) + } + + const handleCancelEventEdit = () => { + clearEventForm() + } + + const handleSaveEvent = (e: React.FormEvent) => { + e.preventDefault() + if (readOnly || !evTime) return + + const eventData = buildEventFromForm() + + if (editingEventIndex !== null) { + const hadSkipperSignature = !!signSkipper + markSkipperSignatureClearedForEventChange() + setEvents((prev) => prev.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 { + setEvents((prev) => [...prev, eventData]) + } + + clearEventForm() } const handleDeleteEvent = (index: number) => { if (readOnly) return + const hadSkipperSignature = !!signSkipper + markSkipperSignatureClearedForEventChange() setEvents((prev) => prev.filter((_, idx) => idx !== index)) + 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) + } } const handleDownloadPdf = async () => { @@ -1079,8 +1154,22 @@ export default function LogEntryEditor({ {ev.remarks} {!readOnly && ( - - + @@ -1095,7 +1184,9 @@ export default function LogEntryEditor({ {/* Add New Event Form Sub-Card */} {!readOnly && (
-

{t('logs.add_event')}

+

+ {editingEventIndex !== null ? t('logs.edit_event') : t('logs.add_event')} +

@@ -1329,16 +1420,30 @@ export default function LogEntryEditor({
- +
+ {editingEventIndex !== null && ( + + )} + +
)} diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index 7a1b741..7a737d2 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -116,6 +116,12 @@ "travel_details": "Reisedetails", "add_event": "Neuen Logbucheintrag hinzufügen", "add_event_btn": "Ereignis hinzufügen", + "edit_event": "Ereignis bearbeiten", + "save_event_btn": "Änderung speichern", + "cancel_event_edit": "Abbrechen", + "delete_event": "Ereignis löschen", + "sign_cleared_skipper_re_sign_title": "Skipper-Unterschrift entfernt", + "sign_cleared_skipper_re_sign": "Das Ereignisprotokoll wurde geändert. Die Skipper-Unterschrift wurde entfernt. Bitte erneut freigeben.", "date": "Datum", "day_of_travel": "Tag der Reise / Reisetag", "departure": "Start-Hafen (Reise von)", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index a5ff535..61db70c 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -116,6 +116,12 @@ "travel_details": "Travel Details", "add_event": "Add Event Log Record", "add_event_btn": "Add Event Entry", + "edit_event": "Edit event", + "save_event_btn": "Save changes", + "cancel_event_edit": "Cancel", + "delete_event": "Delete event", + "sign_cleared_skipper_re_sign_title": "Skipper signature removed", + "sign_cleared_skipper_re_sign": "The event log was changed. The skipper signature was removed. Please sign again.", "date": "Date", "day_of_travel": "Day of Travel", "departure": "Departure Port (von)",