From 658bc6c0c9eeaf9a32a3cf7a6ad82f5bf38c4638 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 31 May 2026 10:57:47 +0200 Subject: [PATCH] feat(logs): Ereignis-Uhrzeit vorbelegen und 24h-Format vereinheitlichen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Ereignisse starten mit der aktuellen Uhrzeit; Datums-/Zeitanzeigen und Zeit-Picker nutzen durchgängig das 24-Stunden-Format. Co-authored-by: Cursor --- client/src/components/LogEntryEditor.tsx | 8 ++-- client/src/components/LogbookBackupPanel.tsx | 5 ++- client/src/components/PasskeySignButton.tsx | 5 +-- client/src/components/SignatureSection.tsx | 5 +-- client/src/services/csvExport.ts | 5 ++- client/src/services/pdfExport.ts | 4 +- client/src/utils/dateTimeFormat.ts | 43 ++++++++++++++++++++ client/src/utils/logEntryPayload.ts | 7 ++++ client/src/utils/seo.ts | 3 +- 9 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 client/src/utils/dateTimeFormat.ts diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index ace98d2..29e7a85 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -22,7 +22,8 @@ import { hasAnySignature } from '../utils/signatures.js' import type { SignatureValue } from '../types/signatures.js' -import { buildLogEntryPayload, sortLogEventsByTime, normalizeLogEvent, logEventsEqual, type LogEventPayload } from '../utils/logEntryPayload.js' +import { buildLogEntryPayload, sortLogEventsByTime, normalizeLogEvent, logEventsEqual, currentLocalTimeHHMM, type LogEventPayload } from '../utils/logEntryPayload.js' +import { resolveIntlLocale } from '../utils/dateTimeFormat.js' import { hashEntryForSigning } from '../utils/entryCanonicalHash.js' import { signLogEntry } from '../services/entrySigning.js' import { getLogbookAccess } from '../services/logbookAccess.js' @@ -162,7 +163,7 @@ export default function LogEntryEditor({ const [events, setEvents] = useState([]) // Add Event Form State - const [evTime, setEvTime] = useState('') + const [evTime, setEvTime] = useState(() => currentLocalTimeHHMM()) const [evMgk, setEvMgk] = useState('') const [evRwk, setEvRwk] = useState('') const [evWindPressure, setEvWindPressure] = useState('') @@ -865,7 +866,7 @@ export default function LogEntryEditor({ } const clearEventForm = () => { - setEvTime('') + setEvTime(currentLocalTimeHHMM()) setEvMgk('') setEvRwk('') setEvWindPressure('') @@ -1371,6 +1372,7 @@ export default function LogEntryEditor({ setEvTime(e.target.value)} disabled={saving} diff --git a/client/src/components/LogbookBackupPanel.tsx b/client/src/components/LogbookBackupPanel.tsx index bd27d5b..734cf46 100644 --- a/client/src/components/LogbookBackupPanel.tsx +++ b/client/src/components/LogbookBackupPanel.tsx @@ -12,6 +12,7 @@ import { type LogbookBackupPreview } from '../services/logbookBackup.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' +import { formatAppDateTime } from '../utils/dateTimeFormat.js' interface LogbookBackupPanelProps { logbookId: string @@ -41,7 +42,7 @@ function mapBackupError(code: string, t: (key: string) => string): string { } export default function LogbookBackupPanel({ logbookId, onRestored }: LogbookBackupPanelProps) { - const { t } = useTranslation() + const { t, i18n } = useTranslation() const { showConfirm } = useDialog() const fileInputRef = useRef(null) @@ -334,7 +335,7 @@ export default function LogbookBackupPanel({ logbookId, onRestored }: LogbookBac

{t('settings.backup_exported_at', { - date: new Date(importPreview.exportedAt).toLocaleString() + date: formatAppDateTime(importPreview.exportedAt, i18n.language) })}

diff --git a/client/src/components/PasskeySignButton.tsx b/client/src/components/PasskeySignButton.tsx index 874780a..b86954b 100644 --- a/client/src/components/PasskeySignButton.tsx +++ b/client/src/components/PasskeySignButton.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Fingerprint, Loader2, AlertTriangle } from 'lucide-react' import type { PasskeySignature } from '../types/signatures.js' +import { formatAppDateTime } from '../utils/dateTimeFormat.js' interface PasskeySignButtonProps { label: string @@ -42,9 +43,7 @@ export default function PasskeySignButton({ } } - const formattedDate = signature - ? new Date(signature.signedAt).toLocaleString(i18n.language === 'de' ? 'de-DE' : 'en-GB') - : '' + const formattedDate = signature ? formatAppDateTime(signature.signedAt, i18n.language) : '' return (
diff --git a/client/src/components/SignatureSection.tsx b/client/src/components/SignatureSection.tsx index fb8484b..14bb5b5 100644 --- a/client/src/components/SignatureSection.tsx +++ b/client/src/components/SignatureSection.tsx @@ -5,6 +5,7 @@ import SignaturePad from './SignaturePad.tsx' import PasskeySignButton from './PasskeySignButton.tsx' import type { PasskeySignature, SignatureValue } from '../types/signatures.js' import { isPasskeySignature, getSignaturePayload, getSignatureAttribution } from '../utils/signatures.js' +import { formatAppDateTime } from '../utils/dateTimeFormat.js' type SignatureMode = 'passkey' | 'classic' @@ -30,9 +31,7 @@ function SignerAttributionBadge({ value }: { value: SignatureValue | '' }) { const attribution = getSignatureAttribution(value) if (!attribution) return null - const formattedDate = new Date(attribution.signedAt).toLocaleString( - i18n.language === 'de' ? 'de-DE' : 'en-GB' - ) + const formattedDate = formatAppDateTime(attribution.signedAt, i18n.language) return (
diff --git a/client/src/services/csvExport.ts b/client/src/services/csvExport.ts index c37fda0..8ca380e 100644 --- a/client/src/services/csvExport.ts +++ b/client/src/services/csvExport.ts @@ -5,6 +5,7 @@ import { decryptJson } from './crypto.js' import { formatSignatureForExport, normalizeSignature } from '../utils/signatures.js' import { sortLogEventsByTime } from '../utils/logEntryPayload.js' import i18n from '../i18n/index.js' +import { formatAppDateTime } from '../utils/dateTimeFormat.js' function escapeCsvValue(val: string | number | undefined | null): string { if (val === null || val === undefined) return ''; @@ -94,11 +95,11 @@ export async function exportLogbookToCsv(logbookId: string, preloadedData?: { ya const exportLabels = { imagePlaceholder: i18n.t('logs.sign_export_image'), passkeyLabel: (username: string, signedAt: string) => { - const date = new Date(signedAt).toLocaleString(i18n.language === 'de' ? 'de-DE' : 'en-GB') + const date = formatAppDateTime(signedAt, i18n.language) return i18n.t('logs.sign_passkey_export', { username, date }) }, attributionLabel: (username: string, signedAt: string) => { - const date = new Date(signedAt).toLocaleString(i18n.language === 'de' ? 'de-DE' : 'en-GB') + const date = formatAppDateTime(signedAt, i18n.language) return i18n.t('logs.sign_attribution_export', { username, date }) } }; diff --git a/client/src/services/pdfExport.ts b/client/src/services/pdfExport.ts index db5179d..3d5ce13 100644 --- a/client/src/services/pdfExport.ts +++ b/client/src/services/pdfExport.ts @@ -6,10 +6,10 @@ import { decryptJson } from './crypto.js' import { isSignatureImage, isPasskeySignature, isClassicSignature, getSignaturePayload } from '../utils/signatures.js' import { sortLogEventsByTime } from '../utils/logEntryPayload.js' import i18n from '../i18n/index.js' +import { formatAppDateTime } from '../utils/dateTimeFormat.js' function formatPasskeySignDate(signedAt: string): string { - const locale = i18n.language === 'de' ? 'de-DE' : 'en-GB' - return new Date(signedAt).toLocaleString(locale) + return formatAppDateTime(signedAt, i18n.language) } export async function generateLogbookPagePdf(logbookId: string, entryId: string, preloadedData?: { yacht: any; entry: any }): Promise { diff --git a/client/src/utils/dateTimeFormat.ts b/client/src/utils/dateTimeFormat.ts new file mode 100644 index 0000000..db0128c --- /dev/null +++ b/client/src/utils/dateTimeFormat.ts @@ -0,0 +1,43 @@ +/** BCP 47 locales that use 24-hour clock for Intl and native pickers. */ +export function resolveIntlLocale(language?: string): string { + const lng = (language ?? 'en').toLowerCase() + return lng.startsWith('de') ? 'de-DE' : 'en-GB' +} + +/** `lang` for `` and `` (24h-friendly). */ +export function resolveDocumentLang(language?: string): string { + const lng = (language ?? 'en').toLowerCase() + return lng.startsWith('de') ? 'de' : 'en-GB' +} + +const APP_DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false +} + +const APP_TIME_OPTIONS: Intl.DateTimeFormatOptions = { + hour: '2-digit', + minute: '2-digit', + hour12: false +} + +function toDate(value: Date | string | number): Date | null { + const date = value instanceof Date ? value : new Date(value) + return Number.isNaN(date.getTime()) ? null : date +} + +export function formatAppDateTime(value: Date | string | number, language?: string): string { + const date = toDate(value) + if (!date) return String(value) + return date.toLocaleString(resolveIntlLocale(language), APP_DATE_TIME_OPTIONS) +} + +export function formatAppTime(value: Date | string | number, language?: string): string { + const date = toDate(value) + if (!date) return String(value) + return date.toLocaleTimeString(resolveIntlLocale(language), APP_TIME_OPTIONS) +} diff --git a/client/src/utils/logEntryPayload.ts b/client/src/utils/logEntryPayload.ts index abcee5a..369cd6f 100644 --- a/client/src/utils/logEntryPayload.ts +++ b/client/src/utils/logEntryPayload.ts @@ -17,6 +17,13 @@ export interface LogEventPayload { remarks: string } +/** Local time as HH:MM for HTML ``. */ +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}` +} + const LOG_EVENT_FIELDS: (keyof LogEventPayload)[] = [ 'time', 'mgk', 'rwk', 'windPressure', 'windDirection', 'windStrength', 'seaState', 'weatherIcon', 'current', 'heel', 'sailsOrMotor', 'logReading', 'distance', diff --git a/client/src/utils/seo.ts b/client/src/utils/seo.ts index 40f6d8d..c4747a0 100644 --- a/client/src/utils/seo.ts +++ b/client/src/utils/seo.ts @@ -1,4 +1,5 @@ import type { i18n as I18nInstance } from 'i18next' +import { resolveDocumentLang } from './dateTimeFormat.js' const SITE_ORIGIN = 'https://kapteins-daagbok.eu' @@ -34,7 +35,7 @@ export function updatePageSeo(lng?: string) { if (!i18nRef?.isInitialized) return const lang = normalizeSeoLang(lng ?? i18nRef.language) - document.documentElement.lang = lang + document.documentElement.lang = resolveDocumentLang(lang) const title = i18nRef.t('seo.title') document.title = title