diff --git a/client/src/components/EventTimeInput24h.tsx b/client/src/components/EventTimeInput24h.tsx index c6ccafd..505a285 100644 --- a/client/src/components/EventTimeInput24h.tsx +++ b/client/src/components/EventTimeInput24h.tsx @@ -10,18 +10,23 @@ interface EventTimeInput24hProps { onChange: (value: string) => void disabled?: boolean 'aria-label'?: string + fallback?: string } export default function EventTimeInput24h({ value, onChange, disabled = false, - 'aria-label': ariaLabel + 'aria-label': ariaLabel, + fallback }: EventTimeInput24hProps) { const baseId = useId() const useNativePicker = preferNativeCameraPicker() - const { hours, minutes } = useMemo(() => splitTimeHHMM(value), [value]) - const timeValue = useMemo(() => joinTimeHHMM(hours, minutes), [hours, minutes]) + const { hours, minutes } = useMemo(() => splitTimeHHMM(value, fallback), [value, fallback]) + const timeValue = useMemo(() => { + if (!value.trim()) return '' + return joinTimeHHMM(hours, minutes) + }, [value, hours, minutes]) if (useNativePicker) { return ( @@ -34,7 +39,7 @@ export default function EventTimeInput24h({ value={timeValue} onChange={(e) => { const next = e.target.value - if (next) onChange(next.slice(0, 5)) + onChange(next ? next.slice(0, 5) : '') }} disabled={disabled} aria-label={ariaLabel} diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index 8914dd5..f787f4a 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -33,7 +33,7 @@ import { hasAnySignature } from '../utils/signatures.js' import type { SignatureValue } from '../types/signatures.js' -import { buildLogEntryPayload, readLogEntryTides, sortLogEventsByTime, normalizeLogEvent, hasUnsavedEventDraft, currentLocalTimeHHMM, isValidTimeHHMM, type LogEventPayload } from '../utils/logEntryPayload.js' +import { buildLogEntryPayload, readLogEntryTides, sortLogEventsByTime, normalizeLogEvent, hasUnsavedEventDraft, currentLocalTimeHHMM, isValidTimeHHMM, type LogEventPayload, type LogEntryTides } from '../utils/logEntryPayload.js' import EventTimeInput24h from './EventTimeInput24h.tsx' import CourseDialInput from './CourseDialInput.tsx' import { parseOwmCurrentWeather } from '../utils/openWeatherMap.js' @@ -432,7 +432,7 @@ export default function LogEntryEditor({ } } - const buildPayloadForSigning = useCallback((eventsOverride?: LogEvent[]) => { + const buildPayloadForSigning = useCallback((eventsOverride?: LogEvent[], tidesOverride?: LogEntryTides) => { return buildLogEntryPayload({ date, dayOfTravel, @@ -451,7 +451,7 @@ export default function LogEntryEditor({ consumption: parseAppDecimalOrZero(fuelConsumption) }, greywater: { level: parseAppDecimalOrZero(greywaterLevel) }, - tides: { highWater: tideHighWater, lowWater: tideLowWater, ...tideLocation }, + tides: tidesOverride ?? { highWater: tideHighWater, lowWater: tideLowWater, ...tideLocation }, trackDistanceNm: parseOptionalFormDecimal(trackDistanceNm), trackSpeedMaxKn: parseOptionalFormDecimal(trackSpeedMaxKn), trackSpeedAvgKn: parseOptionalFormDecimal(trackSpeedAvgKn), @@ -603,12 +603,14 @@ export default function LogEntryEditor({ signCrew?: SignatureValue | '' aiSummary?: string aiSummaryGeneratedAt?: string + tidesOverride?: LogEntryTides } ) => { if (readOnly) return const normalized = Array.isArray(options) ? { eventsOverride: options } : (options ?? {}) const eventsOverride = normalized.eventsOverride + const tidesOverride = normalized.tidesOverride const skipperToSave = normalized.signSkipper !== undefined ? normalized.signSkipper : signSkipper const crewToSave = normalized.signCrew !== undefined ? normalized.signCrew : signCrew let summaryToSave = normalized.aiSummary !== undefined ? normalized.aiSummary : aiSummary @@ -636,7 +638,7 @@ export default function LogEntryEditor({ } const entryData: Record = { - ...buildPayloadForSigning(eventsOverride), + ...buildPayloadForSigning(eventsOverride, tidesOverride), signSkipper: normalizedSerializedSignature(skipperToSave), signCrew: normalizedSerializedSignature(crewToSave) } @@ -1309,14 +1311,27 @@ export default function LogEntryEditor({ } } - const applyTideFetchResult = (result: { + const applyTideFetchResult = async (result: { highWater: string lowWater: string location: TideLocationMeta }) => { - if (result.highWater) setTideHighWater(result.highWater) - if (result.lowWater) setTideLowWater(result.lowWater) + const nextTides = { + highWater: result.highWater, + lowWater: result.lowWater, + ...result.location + } + setTideHighWater(result.highWater) + setTideLowWater(result.lowWater) setTideLocation(result.location) + + try { + await persistEntryToDb({ tidesOverride: nextTides }) + trackPlausibleEvent(PlausibleEvents.TRAVEL_DAY_SAVED) + } catch (err) { + console.error('Failed to auto-save after tide fetch:', err) + showAlert(t('logs.tide_fetch_failed'), t('logs.tide_fetch_btn')) + } } const handleTideStationPick = async (pick: TideFetchNeedsStationPick, station: TideStation) => { @@ -1330,7 +1345,7 @@ export default function LogEntryEditor({ queryLng: pick.queryLng, analyticsSource: 'entry_editor' }) - applyTideFetchResult(result) + await applyTideFetchResult(result) setTideStationPicker(null) } catch (err) { if (err instanceof TidesApiError && err.code === 'NO_DATA_FOR_DATE') { @@ -1377,7 +1392,7 @@ export default function LogEntryEditor({ return } - applyTideFetchResult(outcome as TideFetchResult) + await applyTideFetchResult(outcome as TideFetchResult) } catch (err) { if (err instanceof TidesApiError) { if (err.code === 'OFFLINE') { @@ -2288,6 +2303,7 @@ export default function LogEntryEditor({ onChange={setTideHighWater} disabled={readOnly || saving || tidesLoading} aria-label={t('logs.tide_high_water')} + fallback="00:00" />
@@ -2297,6 +2313,7 @@ export default function LogEntryEditor({ onChange={setTideLowWater} disabled={readOnly || saving || tidesLoading} aria-label={t('logs.tide_low_water')} + fallback="00:00" />
diff --git a/client/src/utils/logEntryPayload.test.ts b/client/src/utils/logEntryPayload.test.ts index cca7eba..262f796 100644 --- a/client/src/utils/logEntryPayload.test.ts +++ b/client/src/utils/logEntryPayload.test.ts @@ -5,6 +5,7 @@ import { isLogEventDraftEmpty, localDateString, normalizeLogEvent, + splitTimeHHMM, type LogEventPayload } from './logEntryPayload.js' @@ -112,3 +113,25 @@ describe('buildLogEntryPayload tides', () => { }) }) }) + +describe('splitTimeHHMM', () => { + it('splits valid time HH:MM correctly', () => { + const result = splitTimeHHMM('15:45') + expect(result).toEqual({ hours: '15', minutes: '45' }) + }) + + it('uses fallback value when time is empty', () => { + const result = splitTimeHHMM('', '00:00') + expect(result).toEqual({ hours: '00', minutes: '00' }) + }) + + it('falls back to current local time when empty and no fallback is specified', () => { + const result = splitTimeHHMM('') + const hours = parseInt(result.hours, 10) + const minutes = parseInt(result.minutes, 10) + expect(hours).toBeGreaterThanOrEqual(0) + expect(hours).toBeLessThanOrEqual(23) + expect(minutes).toBeGreaterThanOrEqual(0) + expect(minutes).toBeLessThanOrEqual(59) + }) +}) diff --git a/client/src/utils/logEntryPayload.ts b/client/src/utils/logEntryPayload.ts index 12dc11e..3a0b489 100644 --- a/client/src/utils/logEntryPayload.ts +++ b/client/src/utils/logEntryPayload.ts @@ -72,8 +72,8 @@ export function isValidTimeHHMM(value: string): boolean { return parseTimeToHHMM(value) !== null } -export function splitTimeHHMM(value: string): { hours: string; minutes: string } { - const parsed = parseTimeToHHMM(value) ?? currentLocalTimeHHMM() +export function splitTimeHHMM(value: string, fallback?: string): { hours: string; minutes: string } { + const parsed = parseTimeToHHMM(value) ?? fallback ?? currentLocalTimeHHMM() return { hours: parsed.slice(0, 2), minutes: parsed.slice(3, 5) } }