Files
kapteins-daagbok/client/src/utils/logEntryPayload.ts
T
elpatron 27c780d2b8 fix: Passkey-Signatur beim Speichern der Logbuchseite erhalten
Leeres Event-Formular (nur Uhrzeit) galt fälschlich als Änderung und
invalidierte frische Signaturen. Speichern-Button und Hash-Sperre folgen
nun echten Entwürfen und synchronisiertem Seiteninhalt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 12:57:02 +02:00

174 lines
5.9 KiB
TypeScript

import {
normalizeCourseAngleString,
normalizeWindDirectionString
} from './courseAngle.js'
export interface LogEventPayload {
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
}
/** Local time as HH:MM (24-hour). */
export function currentLocalTimeHHMM(date: Date = new Date()): string {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
/** Parse 24h or 12h (AM/PM) time strings to HH:MM. */
export function parseTimeToHHMM(value: string): string | null {
const trimmed = value.trim()
if (!trimmed) return null
const amPm = trimmed.match(/^(\d{1,2}):(\d{2})(?::\d{2})?\s*(AM|PM)$/i)
if (amPm) {
let hours = parseInt(amPm[1], 10)
const minutes = parseInt(amPm[2], 10)
const isPm = amPm[3].toUpperCase() === 'PM'
if (hours < 1 || hours > 12 || minutes < 0 || minutes > 59) return null
if (hours === 12) hours = isPm ? 12 : 0
else if (isPm) hours += 12
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
}
const h24 = trimmed.match(/^(\d{1,2}):(\d{2})(?::\d{2})?$/)
if (h24) {
const hours = parseInt(h24[1], 10)
const minutes = parseInt(h24[2], 10)
if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
}
}
return null
}
export function isValidTimeHHMM(value: string): boolean {
return parseTimeToHHMM(value) !== null
}
export function splitTimeHHMM(value: string): { hours: string; minutes: string } {
const parsed = parseTimeToHHMM(value) ?? currentLocalTimeHHMM()
return { hours: parsed.slice(0, 2), minutes: parsed.slice(3, 5) }
}
export function joinTimeHHMM(hours: string, minutes: string): string {
const h = Math.min(23, Math.max(0, parseInt(hours, 10) || 0))
const m = Math.min(59, Math.max(0, parseInt(minutes, 10) || 0))
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
}
const LOG_EVENT_FIELDS: (keyof LogEventPayload)[] = [
'time', 'mgk', 'rwk', 'windPressure', 'windDirection', 'windStrength', 'seaState',
'weatherIcon', 'current', 'heel', 'sailsOrMotor', 'logReading', 'distance',
'gpsLat', 'gpsLng', 'remarks'
]
/** Normalize partial/legacy events so all fields are strings (safe for form + save). */
export function normalizeLogEvent(event: Partial<LogEventPayload> | Record<string, unknown>): LogEventPayload {
const e = event as Record<string, unknown>
const timeRaw = String(e.time ?? '').trim()
const normalized: LogEventPayload = {
time: parseTimeToHHMM(timeRaw) ?? (timeRaw.length >= 5 ? timeRaw.slice(0, 5) : timeRaw),
mgk: normalizeCourseAngleString(String(e.mgk ?? ''), { allowEmpty: true }),
rwk: normalizeCourseAngleString(String(e.rwk ?? ''), { allowEmpty: true }),
windPressure: '',
windDirection: normalizeWindDirectionString(String(e.windDirection ?? '')),
windStrength: '',
seaState: '',
weatherIcon: '',
current: '',
heel: '',
sailsOrMotor: '',
logReading: '',
distance: '',
gpsLat: '',
gpsLng: '',
remarks: ''
}
for (const key of LOG_EVENT_FIELDS) {
if (key === 'time' || key === 'mgk' || key === 'rwk' || key === 'windDirection') continue
normalized[key] = String(e[key] ?? '').trim()
}
return normalized
}
export function logEventsEqual(a: LogEventPayload, b: LogEventPayload): boolean {
return LOG_EVENT_FIELDS.every((key) => a[key] === b[key])
}
const LOG_EVENT_CONTENT_FIELDS = LOG_EVENT_FIELDS.filter((key) => key !== 'time')
/** Draft with only a time (or empty fields) — not an unsaved log entry change. */
export function isLogEventDraftEmpty(event: LogEventPayload): boolean {
return LOG_EVENT_CONTENT_FIELDS.every((key) => !event[key]?.trim())
}
/** Whether the event form holds unsaved changes worth merging on page save. */
export function hasUnsavedEventDraft(
draft: LogEventPayload,
editingEventIndex: number | null,
events: LogEventPayload[]
): boolean {
if (!isValidTimeHHMM(draft.time)) return false
if (editingEventIndex !== null) {
const original = events[editingEventIndex]
return original ? !logEventsEqual(draft, original) : false
}
return !isLogEventDraftEmpty(draft)
}
/** Chronological order: earliest time first (HH:MM). */
export function sortLogEventsByTime<T extends LogEventPayload>(events: T[]): T[] {
return [...events].sort((a, b) => (a.time || '').localeCompare(b.time || ''))
}
export interface LogEntryPayloadInput {
date: string
dayOfTravel: string
departure: string
destination: string
freshwater: { morning: number; refilled: number; evening: number; consumption: number }
fuel: { morning: number; refilled: number; evening: number; consumption: number }
trackDistanceNm?: number
trackSpeedMaxKn?: number
trackSpeedAvgKn?: number
motorHours?: number
events: LogEventPayload[]
}
export function buildLogEntryPayload(input: LogEntryPayloadInput): Record<string, unknown> {
const payload: Record<string, unknown> = {
date: input.date,
dayOfTravel: input.dayOfTravel.trim(),
departure: input.departure.trim(),
destination: input.destination.trim(),
freshwater: { ...input.freshwater },
fuel: { ...input.fuel },
events: sortLogEventsByTime(input.events.map((e) => normalizeLogEvent(e)))
}
if (input.trackDistanceNm !== undefined) payload.trackDistanceNm = input.trackDistanceNm
if (input.trackSpeedMaxKn !== undefined) payload.trackSpeedMaxKn = input.trackSpeedMaxKn
if (input.trackSpeedAvgKn !== undefined) payload.trackSpeedAvgKn = input.trackSpeedAvgKn
if (input.motorHours !== undefined && input.motorHours > 0) {
payload.motorHours = Number(input.motorHours.toFixed(2))
}
return payload
}