Compare commits

...

2 Commits

Author SHA1 Message Date
elpatron 5f9e83dbdd chore: release v0.1.0.27 2026-05-30 10:56:53 +02:00
elpatron aa2b35ddac feat: Ereignisprotokoll bearbeiten und Skipper-Signatur invalidieren
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 <cursoragent@cursor.com>
2026-05-30 10:56:45 +02:00
5 changed files with 174 additions and 45 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.0.27
0.1.0.28
+12
View File
@@ -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);
}
+149 -44
View File
@@ -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<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)
@@ -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({
</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 +1184,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 +1420,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>
+6
View File
@@ -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)",
+6
View File
@@ -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)",