From 3b94727c079885206f2c6004613a649d470ef2ff Mon Sep 17 00:00:00 2001 From: jubnl Date: Fri, 17 Apr 2026 16:59:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(journey):=20fix=20issue=20#704=20=E2=80=94?= =?UTF-8?q?=20active=20logic,=20archive,=20places=20rename,=20search,=20tr?= =?UTF-8?q?ip=20reminders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Derive journey lifecycle from linked trip dates (live/upcoming/completed/draft) instead of relying solely on status field; status=archived always wins - Add Archive/Restore Journey action in journey settings dialog - Rename cities → places end-to-end (SQL alias, TS types, stats field, all locales) - Wire up search icon: toggles inline input, filters by title+subtitle client-side - Fix channelConfigured check: trip reminders enabled by default since inapp is always available; remove channel check, controlled solely by admin setting - Expose notify_trip_reminder toggle in Admin → Settings → Notifications - Add trip_date_min/trip_date_max to listJourneys SQL for client-side lifecycle - Add archived status to Journey type (server + client) - Update all 15 locale files with new keys (search, archive, places, trip reminders) --- client/src/i18n/translations/ar.ts | 12 ++ client/src/i18n/translations/br.ts | 12 ++ client/src/i18n/translations/cs.ts | 12 ++ client/src/i18n/translations/de.ts | 12 ++ client/src/i18n/translations/en.ts | 12 ++ client/src/i18n/translations/es.ts | 12 ++ client/src/i18n/translations/fr.ts | 12 ++ client/src/i18n/translations/hu.ts | 12 ++ client/src/i18n/translations/id.ts | 12 ++ client/src/i18n/translations/it.ts | 12 ++ client/src/i18n/translations/nl.ts | 12 ++ client/src/i18n/translations/pl.ts | 12 ++ client/src/i18n/translations/ru.ts | 12 ++ client/src/i18n/translations/zh.ts | 12 ++ client/src/i18n/translations/zhTw.ts | 12 ++ client/src/pages/AdminPage.tsx | 32 +++++ client/src/pages/JourneyDetailPage.test.tsx | 51 ++++--- client/src/pages/JourneyDetailPage.tsx | 62 ++++++-- client/src/pages/JourneyPage.test.tsx | 24 ++-- client/src/pages/JourneyPage.tsx | 133 ++++++++++++++---- client/src/pages/JourneyPublicPage.test.tsx | 6 +- client/src/pages/JourneyPublicPage.tsx | 2 +- client/src/store/journeyStore.ts | 4 +- client/src/utils/journeyLifecycle.ts | 32 +++++ server/src/scheduler.ts | 9 +- server/src/services/authService.ts | 4 +- server/src/services/journeyService.ts | 12 +- server/src/services/journeyShareService.ts | 2 +- server/src/types.ts | 2 +- .../unit/services/journeyService.test.ts | 37 ++++- .../unit/services/journeyShareService.test.ts | 4 +- 31 files changed, 507 insertions(+), 89 deletions(-) create mode 100644 client/src/utils/journeyLifecycle.ts diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 78a252d4..e4a7ef49 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -1570,6 +1570,14 @@ const ar: Record = { 'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟', 'memories.confirmShareHint': '{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.', 'memories.confirmShareButton': 'مشاركة الصور', + 'journey.search.placeholder': 'البحث في الرحلات…', + 'journey.search.noResults': 'لا توجد رحلات تطابق "{query}"', + 'journey.status.archived': 'مؤرشف', + 'journey.settings.endJourney': 'أرشفة الرحلة', + 'journey.settings.reopenJourney': 'استعادة الرحلة', + 'journey.settings.archived': 'تم أرشفة الرحلة', + 'journey.settings.reopened': 'تمت إعادة فتح الرحلة', + 'journey.settings.endDescription': 'يخفي شارة البث المباشر. يمكنك إعادة الفتح في أي وقت.', 'journey.settings.failedToDelete': 'فشل في الحذف', 'journey.entries.deleteTitle': 'حذف الإدخال', 'journey.photosUploaded': 'تم رفع {count} صورة', @@ -1876,6 +1884,10 @@ const ar: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'فشل إرسال Ntfy التجريبي', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'يُرسل Ntfy للمسؤول دائمًا عند تهيئة موضوع', 'admin.notifications.adminNotificationsHint': 'حدد القنوات التي تُسلّم إشعارات المسؤول (مثل تنبيهات الإصدارات). يُرسل الـ Webhook تلقائيًا عند تعيين رابط URL لـ Webhook المسؤول.', + 'admin.notifications.tripReminders.title': 'تذكيرات الرحلات', + 'admin.notifications.tripReminders.hint': 'إرسال تذكير قبل بدء الرحلة (يتطلب تعيين أيام التذكير على الرحلة).', + 'admin.notifications.tripReminders.enabled': 'تم تفعيل تذكيرات الرحلات', + 'admin.notifications.tripReminders.disabled': 'تم تعطيل تذكيرات الرحلات', 'admin.tabs.notifications': 'الإشعارات', 'notifications.versionAvailable.title': 'تحديث متاح', 'notifications.versionAvailable.text': 'TREK {version} متاح الآن.', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index c332301e..8c4d0c4f 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -1825,6 +1825,10 @@ const br: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Falha ao enviar Ntfy de teste', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'O Ntfy de admin sempre dispara quando um tópico está configurado', 'admin.notifications.adminNotificationsHint': 'Configure quais canais entregam notificações de admin (ex. alertas de versão). O webhook dispara automaticamente se uma URL de webhook de admin estiver definida.', + 'admin.notifications.tripReminders.title': 'Lembretes de viagem', + 'admin.notifications.tripReminders.hint': 'Envia uma notificação de lembrete antes do início de uma viagem (requer dias de lembrete definidos na viagem).', + 'admin.notifications.tripReminders.enabled': 'Lembretes de viagem ativados', + 'admin.notifications.tripReminders.disabled': 'Lembretes de viagem desativados', 'admin.tabs.notifications': 'Notificações', 'notifications.versionAvailable.title': 'Atualização disponível', 'notifications.versionAvailable.text': 'TREK {version} já está disponível.', @@ -1872,6 +1876,8 @@ const br: Record = { 'memories.saveRouteNotConfigured': 'A rota de salvamento não está configurada para este provedor', 'memories.testRouteNotConfigured': 'A rota de teste não está configurada para este provedor', 'memories.fillRequiredFields': 'Por favor preencha todos os campos obrigatórios', + 'journey.search.placeholder': 'Buscar jornadas…', + 'journey.search.noResults': 'Nenhuma jornada corresponde a "{query}"', 'journey.title': 'Jornada', 'journey.subtitle': 'Registre suas viagens em tempo real', 'journey.new': 'Nova jornada', @@ -1893,6 +1899,7 @@ const br: Record = { 'journey.status.active': 'Ativa', 'journey.status.completed': 'Concluída', 'journey.status.upcoming': 'Próxima', + 'journey.status.archived': 'Arquivado', 'journey.checkin.add': 'Fazer check-in', 'journey.checkin.namePlaceholder': 'Nome do local', 'journey.checkin.notesPlaceholder': 'Notas (opcional)', @@ -2046,6 +2053,11 @@ const br: Record = { 'journey.settings.name': 'Nome', 'journey.settings.subtitle': 'Subtítulo', 'journey.settings.subtitlePlaceholder': 'ex. Tailândia, Vietnã e Camboja', + 'journey.settings.endJourney': 'Arquivar Jornada', + 'journey.settings.reopenJourney': 'Restaurar Jornada', + 'journey.settings.archived': 'Jornada arquivada', + 'journey.settings.reopened': 'Jornada reaberta', + 'journey.settings.endDescription': 'Oculta o selo Ao Vivo. Você pode reabrir a qualquer momento.', 'journey.settings.delete': 'Excluir', 'journey.settings.deleteJourney': 'Excluir jornada', 'journey.settings.deleteMessage': 'Excluir "{title}"? Todas as entradas e fotos serão perdidas.', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 9bda2153..a7185359 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -1830,6 +1830,10 @@ const cs: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Testovací Ntfy selhalo', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin Ntfy odesílá vždy, když je nakonfigurováno téma', 'admin.notifications.adminNotificationsHint': 'Nastavte, které kanály doručují admin oznámení (např. upozornění na verze). Webhook odesílá automaticky, pokud je nastavena URL admin webhooku.', + 'admin.notifications.tripReminders.title': 'Připomínky výletů', + 'admin.notifications.tripReminders.hint': 'Odešle upozornění před začátkem výletu (vyžaduje nastavené dny připomínky na výletu).', + 'admin.notifications.tripReminders.enabled': 'Připomínky výletů aktivovány', + 'admin.notifications.tripReminders.disabled': 'Připomínky výletů deaktivovány', 'admin.tabs.notifications': 'Oznámení', 'notifications.versionAvailable.title': 'Dostupná aktualizace', 'notifications.versionAvailable.text': 'TREK {version} je nyní k dispozici.', @@ -1877,6 +1881,8 @@ const cs: Record = { 'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele', 'memories.testRouteNotConfigured': 'Testovací trasa není nakonfigurována pro tohoto poskytovatele', 'memories.fillRequiredFields': 'Prosím vyplňte všechna povinná pole', + 'journey.search.placeholder': 'Hledat cesty…', + 'journey.search.noResults': 'Žádné cesty neodpovídají „{query}"', 'journey.title': 'Cestovní deník', 'journey.subtitle': 'Zaznamenávejte své cesty průběžně', 'journey.new': 'Nový cestovní deník', @@ -1898,6 +1904,7 @@ const cs: Record = { 'journey.status.active': 'Aktivní', 'journey.status.completed': 'Dokončeno', 'journey.status.upcoming': 'Nadcházející', + 'journey.status.archived': 'Archivováno', 'journey.checkin.add': 'Odbavit se', 'journey.checkin.namePlaceholder': 'Název místa', 'journey.checkin.notesPlaceholder': 'Poznámky (volitelné)', @@ -2051,6 +2058,11 @@ const cs: Record = { 'journey.settings.name': 'Název', 'journey.settings.subtitle': 'Podtitul', 'journey.settings.subtitlePlaceholder': 'např. Thajsko, Vietnam a Kambodža', + 'journey.settings.endJourney': 'Archivovat cestu', + 'journey.settings.reopenJourney': 'Obnovit cestu', + 'journey.settings.archived': 'Cesta archivována', + 'journey.settings.reopened': 'Cesta znovu otevřena', + 'journey.settings.endDescription': 'Skryje odznak Živě. Kdykoli jej lze znovu otevřít.', 'journey.settings.delete': 'Smazat', 'journey.settings.deleteJourney': 'Smazat cestovní deník', 'journey.settings.deleteMessage': 'Smazat „{title}"? Všechny záznamy a fotky budou ztraceny.', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index dcf9492e..2d8f39a6 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -1833,6 +1833,10 @@ const de: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Test-Ntfy fehlgeschlagen', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin-Ntfy sendet immer, wenn ein Thema konfiguriert ist', 'admin.notifications.adminNotificationsHint': 'Konfiguriere, welche Kanäle Admin-Benachrichtigungen liefern (z. B. Versions-Updates). Der Webhook sendet automatisch, wenn eine Admin-Webhook-URL gesetzt ist.', + 'admin.notifications.tripReminders.title': 'Reiseerinnerungen', + 'admin.notifications.tripReminders.hint': 'Sendet eine Erinnerungsbenachrichtigung vor Reisebeginn (erfordert gesetzte Erinnerungstage bei der Reise).', + 'admin.notifications.tripReminders.enabled': 'Reiseerinnerungen aktiviert', + 'admin.notifications.tripReminders.disabled': 'Reiseerinnerungen deaktiviert', 'admin.tabs.notifications': 'Benachrichtigungen', 'notifications.versionAvailable.title': 'Update verfügbar', 'notifications.versionAvailable.text': 'TREK {version} ist jetzt verfügbar.', @@ -1874,6 +1878,8 @@ const de: Record = { 'notif.dev.unknown_event.text': 'Ereignistyp "{event}" ist nicht in EVENT_NOTIFICATION_CONFIG registriert', // Journey Addon + 'journey.search.placeholder': 'Reisen suchen…', + 'journey.search.noResults': 'Keine Reisen passen zu „{query}"', 'journey.title': 'Journey', 'journey.subtitle': 'Dokumentiere deine Reisen unterwegs', 'journey.new': 'Neue Journey', @@ -1895,6 +1901,7 @@ const de: Record = { 'journey.status.active': 'Aktiv', 'journey.status.completed': 'Abgeschlossen', 'journey.status.upcoming': 'Anstehend', + 'journey.status.archived': 'Archiviert', 'journey.checkin.add': 'Einchecken', 'journey.checkin.namePlaceholder': 'Ortsname', 'journey.checkin.notesPlaceholder': 'Notizen (optional)', @@ -2052,6 +2059,11 @@ const de: Record = { 'journey.settings.name': 'Name', 'journey.settings.subtitle': 'Untertitel', 'journey.settings.subtitlePlaceholder': 'z.B. Thailand, Vietnam & Kambodscha', + 'journey.settings.endJourney': 'Reise archivieren', + 'journey.settings.reopenJourney': 'Reise wiederherstellen', + 'journey.settings.archived': 'Reise archiviert', + 'journey.settings.reopened': 'Reise erneut geöffnet', + 'journey.settings.endDescription': 'Blendet das Live-Abzeichen aus. Sie können jederzeit wieder öffnen.', 'journey.settings.delete': 'Löschen', 'journey.settings.deleteJourney': 'Journey löschen', 'journey.settings.deleteMessage': '"{title}" löschen? Alle Einträge und Fotos gehen verloren.', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index e0fb72c7..d5b0b1d8 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -251,6 +251,10 @@ const en: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Test ntfy failed', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin ntfy always fires when a topic is configured', 'admin.notifications.adminNotificationsHint': 'Configure which channels deliver admin-only notifications (e.g. version alerts).', + 'admin.notifications.tripReminders.title': 'Trip Reminders', + 'admin.notifications.tripReminders.hint': 'Send a reminder notification before a trip starts (requires reminder days to be set on the trip).', + 'admin.notifications.tripReminders.enabled': 'Trip reminders enabled', + 'admin.notifications.tripReminders.disabled': 'Trip reminders disabled', 'admin.smtp.title': 'Email & Notifications', 'admin.smtp.hint': 'SMTP configuration for sending email notifications.', 'admin.smtp.testButton': 'Send test email', @@ -1877,6 +1881,8 @@ const en: Record = { 'notif.dev.unknown_event.text': 'Event type "{event}" is not registered in EVENT_NOTIFICATION_CONFIG', // Journey addon + 'journey.search.placeholder': 'Search journeys…', + 'journey.search.noResults': 'No journeys match "{query}"', 'journey.title': 'Journey', 'journey.subtitle': 'Track your travels as they happen', 'journey.new': 'New Journey', @@ -1898,6 +1904,7 @@ const en: Record = { 'journey.status.active': 'Active', 'journey.status.completed': 'Completed', 'journey.status.upcoming': 'Upcoming', + 'journey.status.archived': 'Archived', 'journey.checkin.add': 'Check in', 'journey.checkin.namePlaceholder': 'Location name', 'journey.checkin.notesPlaceholder': 'Notes (optional)', @@ -2076,6 +2083,11 @@ const en: Record = { 'journey.settings.name': 'Name', 'journey.settings.subtitle': 'Subtitle', 'journey.settings.subtitlePlaceholder': 'e.g. Thailand, Vietnam & Cambodia', + 'journey.settings.endJourney': 'Archive Journey', + 'journey.settings.reopenJourney': 'Restore Journey', + 'journey.settings.archived': 'Journey archived', + 'journey.settings.reopened': 'Journey reopened', + 'journey.settings.endDescription': 'Hides the Live badge. You can reopen anytime.', 'journey.settings.delete': 'Delete', 'journey.settings.deleteJourney': 'Delete Journey', 'journey.settings.deleteMessage': 'Delete "{title}"? All entries and photos will be lost.', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 2de8f1c7..13b5854f 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1835,6 +1835,10 @@ const es: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Error al enviar el Ntfy de prueba', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'El Ntfy de admin siempre se activa cuando hay un tema configurado', 'admin.notifications.adminNotificationsHint': 'Configura qué canales entregan notificaciones de admin (ej. alertas de versión). El webhook se activa automáticamente si hay una URL de webhook de admin configurada.', + 'admin.notifications.tripReminders.title': 'Recordatorios de viaje', + 'admin.notifications.tripReminders.hint': 'Envía una notificación de recordatorio antes de que comience un viaje (requiere días de recordatorio configurados en el viaje).', + 'admin.notifications.tripReminders.enabled': 'Recordatorios de viaje activados', + 'admin.notifications.tripReminders.disabled': 'Recordatorios de viaje desactivados', 'admin.tabs.notifications': 'Notificaciones', 'notifications.versionAvailable.title': 'Actualización disponible', 'notifications.versionAvailable.text': 'TREK {version} ya está disponible.', @@ -1879,6 +1883,8 @@ const es: Record = { 'common.justNow': 'justo ahora', 'common.hoursAgo': 'hace {count}h', 'common.daysAgo': 'hace {count}d', + 'journey.search.placeholder': 'Buscar viajes…', + 'journey.search.noResults': 'Ningún viaje coincide con "{query}"', 'journey.title': 'Travesía', 'journey.subtitle': 'Registra tus viajes en tiempo real', 'journey.new': 'Nueva travesía', @@ -1900,6 +1906,7 @@ const es: Record = { 'journey.status.active': 'Activa', 'journey.status.completed': 'Completada', 'journey.status.upcoming': 'Próxima', + 'journey.status.archived': 'Archivado', 'journey.checkin.add': 'Registrar ubicación', 'journey.checkin.namePlaceholder': 'Nombre del lugar', 'journey.checkin.notesPlaceholder': 'Notas (opcional)', @@ -2053,6 +2060,11 @@ const es: Record = { 'journey.settings.name': 'Nombre', 'journey.settings.subtitle': 'Subtítulo', 'journey.settings.subtitlePlaceholder': 'p. ej. Tailandia, Vietnam y Camboya', + 'journey.settings.endJourney': 'Archivar viaje', + 'journey.settings.reopenJourney': 'Restaurar viaje', + 'journey.settings.archived': 'Viaje archivado', + 'journey.settings.reopened': 'Viaje reabierto', + 'journey.settings.endDescription': 'Oculta la insignia En Vivo. Puedes reabrirlo en cualquier momento.', 'journey.settings.delete': 'Eliminar', 'journey.settings.deleteJourney': 'Eliminar travesía', 'journey.settings.deleteMessage': '¿Eliminar "{title}"? Todas las entradas y fotos se perderán.', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index e9de6556..2a1d6fcc 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -1829,6 +1829,10 @@ const fr: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Échec de l\'envoi du Ntfy de test', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Le Ntfy admin s\'active toujours lorsqu\'un sujet est configuré', 'admin.notifications.adminNotificationsHint': 'Configurez quels canaux envoient les notifications admin (ex. alertes de version). Le webhook s\'active automatiquement si une URL webhook admin est définie.', + 'admin.notifications.tripReminders.title': 'Rappels de voyage', + 'admin.notifications.tripReminders.hint': 'Envoie une notification de rappel avant le début d\'un voyage (nécessite des jours de rappel définis sur le voyage).', + 'admin.notifications.tripReminders.enabled': 'Rappels de voyage activés', + 'admin.notifications.tripReminders.disabled': 'Rappels de voyage désactivés', 'admin.tabs.notifications': 'Notifications', 'notifications.versionAvailable.title': 'Mise à jour disponible', 'notifications.versionAvailable.text': 'TREK {version} est maintenant disponible.', @@ -1873,6 +1877,8 @@ const fr: Record = { 'common.justNow': 'à l\'instant', 'common.hoursAgo': 'il y a {count}h', 'common.daysAgo': 'il y a {count}j', + 'journey.search.placeholder': 'Rechercher des journaux…', + 'journey.search.noResults': 'Aucun journal ne correspond à « {query} »', 'journey.title': 'Journal de voyage', 'journey.subtitle': 'Suivez vos voyages en temps réel', 'journey.new': 'Nouveau journal', @@ -1894,6 +1900,7 @@ const fr: Record = { 'journey.status.active': 'Actif', 'journey.status.completed': 'Terminé', 'journey.status.upcoming': 'À venir', + 'journey.status.archived': 'Archivé', 'journey.checkin.add': 'Check-in', 'journey.checkin.namePlaceholder': 'Nom du lieu', 'journey.checkin.notesPlaceholder': 'Notes (facultatif)', @@ -2047,6 +2054,11 @@ const fr: Record = { 'journey.settings.name': 'Nom', 'journey.settings.subtitle': 'Sous-titre', 'journey.settings.subtitlePlaceholder': 'ex. Thaïlande, Vietnam et Cambodge', + 'journey.settings.endJourney': 'Archiver le journal', + 'journey.settings.reopenJourney': 'Restaurer le journal', + 'journey.settings.archived': 'Journal archivé', + 'journey.settings.reopened': 'Journal rouvert', + 'journey.settings.endDescription': 'Masque l\'indicateur En direct. Vous pouvez rouvrir à tout moment.', 'journey.settings.delete': 'Supprimer', 'journey.settings.deleteJourney': 'Supprimer le journal', 'journey.settings.deleteMessage': 'Supprimer « {title} » ? Toutes les entrées et photos seront perdues.', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index f02ed627..462186f0 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -1827,6 +1827,10 @@ const hu: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Teszt Ntfy sikertelen', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Az admin Ntfy mindig küld, ha egy téma konfigurálva van', 'admin.notifications.adminNotificationsHint': 'Állítsa be, hogy mely csatornák szállítsák az admin értesítéseket (pl. verziófrissítési figyelmeztetések). A webhook automatikusan küld, ha admin webhook URL van megadva.', + 'admin.notifications.tripReminders.title': 'Utazási emlékeztetők', + 'admin.notifications.tripReminders.hint': 'Emlékeztető értesítést küld az utazás kezdete előtt (az utazásnál megadott emlékeztető napok szükségesek).', + 'admin.notifications.tripReminders.enabled': 'Utazási emlékeztetők engedélyezve', + 'admin.notifications.tripReminders.disabled': 'Utazási emlékeztetők letiltva', 'admin.tabs.notifications': 'Értesítések', 'notifications.versionAvailable.title': 'Elérhető frissítés', 'notifications.versionAvailable.text': 'A TREK {version} már elérhető.', @@ -1874,6 +1878,8 @@ const hu: Record = { 'memories.saveRouteNotConfigured': 'A mentési útvonal nincs konfigurálva ehhez a szolgáltatóhoz', 'memories.testRouteNotConfigured': 'A tesztútvonal nincs konfigurálva ehhez a szolgáltatóhoz', 'memories.fillRequiredFields': 'Kérjük töltse ki az összes kötelező mezőt', + 'journey.search.placeholder': 'Utak keresése…', + 'journey.search.noResults': 'Nincs „{query}" kifejezéssel egyező út', 'journey.title': 'Útinaplók', 'journey.subtitle': 'Kövesse nyomon utazásait valós időben', 'journey.new': 'Új útinapló', @@ -1895,6 +1901,7 @@ const hu: Record = { 'journey.status.active': 'Aktív', 'journey.status.completed': 'Befejezett', 'journey.status.upcoming': 'Közelgő', + 'journey.status.archived': 'Archivált', 'journey.checkin.add': 'Bejelentkezés', 'journey.checkin.namePlaceholder': 'Helyszín neve', 'journey.checkin.notesPlaceholder': 'Jegyzetek (opcionális)', @@ -2048,6 +2055,11 @@ const hu: Record = { 'journey.settings.name': 'Név', 'journey.settings.subtitle': 'Alcím', 'journey.settings.subtitlePlaceholder': 'pl. Thaiföld, Vietnam és Kambodzsa', + 'journey.settings.endJourney': 'Út archiválása', + 'journey.settings.reopenJourney': 'Út visszaállítása', + 'journey.settings.archived': 'Út archiválva', + 'journey.settings.reopened': 'Út újranyitva', + 'journey.settings.endDescription': 'Elrejti az Élő jelzést. Bármikor újranyitható.', 'journey.settings.delete': 'Törlés', 'journey.settings.deleteJourney': 'Útinapló törlése', 'journey.settings.deleteMessage': 'Törlöd a(z) „{title}" útinaplót? Minden bejegyzés és fotó elveszik.', diff --git a/client/src/i18n/translations/id.ts b/client/src/i18n/translations/id.ts index d00b12d5..5e7a0f1e 100644 --- a/client/src/i18n/translations/id.ts +++ b/client/src/i18n/translations/id.ts @@ -251,6 +251,10 @@ const id: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Uji Ntfy gagal', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin Ntfy selalu berjalan jika topik dikonfigurasi', 'admin.notifications.adminNotificationsHint': 'Atur saluran mana yang mengirimkan notifikasi khusus admin (mis. peringatan versi).', + 'admin.notifications.tripReminders.title': 'Pengingat Perjalanan', + 'admin.notifications.tripReminders.hint': 'Mengirim notifikasi pengingat sebelum perjalanan dimulai (memerlukan hari pengingat yang diatur pada perjalanan).', + 'admin.notifications.tripReminders.enabled': 'Pengingat perjalanan diaktifkan', + 'admin.notifications.tripReminders.disabled': 'Pengingat perjalanan dinonaktifkan', 'admin.smtp.title': 'Email & Notifikasi', 'admin.smtp.hint': 'Konfigurasi SMTP untuk pengiriman notifikasi email.', 'admin.smtp.testButton': 'Kirim email uji', @@ -1877,6 +1881,8 @@ const id: Record = { 'notif.dev.unknown_event.text': 'Tipe event "{event}" tidak terdaftar di EVENT_NOTIFICATION_CONFIG', // Journey addon + 'journey.search.placeholder': 'Cari perjalanan…', + 'journey.search.noResults': 'Tidak ada perjalanan yang cocok dengan "{query}"', 'journey.title': 'Journey', 'journey.subtitle': 'Lacak perjalananmu saat terjadi', 'journey.new': 'Journey Baru', @@ -1898,6 +1904,7 @@ const id: Record = { 'journey.status.active': 'Aktif', 'journey.status.completed': 'Selesai', 'journey.status.upcoming': 'Mendatang', + 'journey.status.archived': 'Diarsipkan', 'journey.checkin.add': 'Check in', 'journey.checkin.namePlaceholder': 'Nama lokasi', 'journey.checkin.notesPlaceholder': 'Catatan (opsional)', @@ -2075,6 +2082,11 @@ const id: Record = { 'journey.settings.name': 'Nama', 'journey.settings.subtitle': 'Subjudul', 'journey.settings.subtitlePlaceholder': 'mis. Thailand, Vietnam & Kamboja', + 'journey.settings.endJourney': 'Arsipkan Perjalanan', + 'journey.settings.reopenJourney': 'Pulihkan Perjalanan', + 'journey.settings.archived': 'Perjalanan diarsipkan', + 'journey.settings.reopened': 'Perjalanan dibuka kembali', + 'journey.settings.endDescription': 'Menyembunyikan lencana Langsung. Anda dapat membuka kembali kapan saja.', 'journey.settings.delete': 'Hapus', 'journey.settings.deleteJourney': 'Hapus Journey', 'journey.settings.deleteMessage': 'Hapus "{title}"? Semua entri dan foto akan hilang.', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 5c882695..58ecfea6 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -1830,6 +1830,10 @@ const it: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Invio Ntfy di test fallito', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Il Ntfy admin si attiva sempre quando un argomento è configurato', 'admin.notifications.adminNotificationsHint': 'Configura quali canali consegnano le notifiche admin (es. avvisi di versione). Il webhook si attiva automaticamente se è impostato un URL webhook admin.', + 'admin.notifications.tripReminders.title': 'Promemoria viaggio', + 'admin.notifications.tripReminders.hint': 'Invia una notifica promemoria prima dell\'inizio di un viaggio (richiede giorni di promemoria impostati sul viaggio).', + 'admin.notifications.tripReminders.enabled': 'Promemoria viaggio attivati', + 'admin.notifications.tripReminders.disabled': 'Promemoria viaggio disattivati', 'admin.tabs.notifications': 'Notifiche', 'notifications.versionAvailable.title': 'Aggiornamento disponibile', 'notifications.versionAvailable.text': 'TREK {version} è ora disponibile.', @@ -1874,6 +1878,8 @@ const it: Record = { 'common.justNow': 'proprio ora', 'common.hoursAgo': '{count}h fa', 'common.daysAgo': '{count}g fa', + 'journey.search.placeholder': 'Cerca viaggi…', + 'journey.search.noResults': 'Nessun viaggio corrisponde a "{query}"', 'journey.title': 'Diario di viaggio', 'journey.subtitle': 'Segui i tuoi viaggi in tempo reale', 'journey.new': 'Nuovo diario', @@ -1895,6 +1901,7 @@ const it: Record = { 'journey.status.active': 'Attivo', 'journey.status.completed': 'Completato', 'journey.status.upcoming': 'In arrivo', + 'journey.status.archived': 'Archiviato', 'journey.checkin.add': 'Check-in', 'journey.checkin.namePlaceholder': 'Nome del luogo', 'journey.checkin.notesPlaceholder': 'Note (facoltativo)', @@ -2048,6 +2055,11 @@ const it: Record = { 'journey.settings.name': 'Nome', 'journey.settings.subtitle': 'Sottotitolo', 'journey.settings.subtitlePlaceholder': 'es. Thailandia, Vietnam e Cambogia', + 'journey.settings.endJourney': 'Archivia il viaggio', + 'journey.settings.reopenJourney': 'Ripristina il viaggio', + 'journey.settings.archived': 'Viaggio archiviato', + 'journey.settings.reopened': 'Viaggio riaperto', + 'journey.settings.endDescription': 'Nasconde il badge In diretta. Puoi riaprire in qualsiasi momento.', 'journey.settings.delete': 'Elimina', 'journey.settings.deleteJourney': 'Elimina diario', 'journey.settings.deleteMessage': 'Eliminare "{title}"? Tutte le voci e le foto andranno perse.', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 82620e2d..cc9d97cd 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -1829,6 +1829,10 @@ const nl: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Test-Ntfy mislukt', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin-Ntfy verstuurt altijd wanneer een onderwerp is geconfigureerd', 'admin.notifications.adminNotificationsHint': 'Stel in via welke kanalen admin-meldingen worden bezorgd (bijv. versie-updates). De webhook verstuurt automatisch als er een admin-webhook-URL is ingesteld.', + 'admin.notifications.tripReminders.title': 'Reisherinneringen', + 'admin.notifications.tripReminders.hint': 'Stuurt een herinneringsmelding voor de start van een reis (vereist ingestelde herinneringsdagen bij de reis).', + 'admin.notifications.tripReminders.enabled': 'Reisherinneringen ingeschakeld', + 'admin.notifications.tripReminders.disabled': 'Reisherinneringen uitgeschakeld', 'admin.tabs.notifications': 'Meldingen', 'notifications.versionAvailable.title': 'Update beschikbaar', 'notifications.versionAvailable.text': 'TREK {version} is nu beschikbaar.', @@ -1873,6 +1877,8 @@ const nl: Record = { 'common.justNow': 'zojuist', 'common.hoursAgo': '{count}u geleden', 'common.daysAgo': '{count}d geleden', + 'journey.search.placeholder': 'Reizen zoeken…', + 'journey.search.noResults': 'Geen reizen komen overeen met "{query}"', 'journey.title': 'Reisverslag', 'journey.subtitle': 'Leg je reizen vast terwijl je onderweg bent', 'journey.new': 'Nieuw reisverslag', @@ -1894,6 +1900,7 @@ const nl: Record = { 'journey.status.active': 'Actief', 'journey.status.completed': 'Voltooid', 'journey.status.upcoming': 'Gepland', + 'journey.status.archived': 'Gearchiveerd', 'journey.checkin.add': 'Inchecken', 'journey.checkin.namePlaceholder': 'Locatienaam', 'journey.checkin.notesPlaceholder': 'Notities (optioneel)', @@ -2047,6 +2054,11 @@ const nl: Record = { 'journey.settings.name': 'Naam', 'journey.settings.subtitle': 'Ondertitel', 'journey.settings.subtitlePlaceholder': 'bijv. Thailand, Vietnam & Cambodja', + 'journey.settings.endJourney': 'Reis archiveren', + 'journey.settings.reopenJourney': 'Reis herstellen', + 'journey.settings.archived': 'Reis gearchiveerd', + 'journey.settings.reopened': 'Reis heropend', + 'journey.settings.endDescription': 'Verbergt het Live-badge. Je kunt het altijd heropenen.', 'journey.settings.delete': 'Verwijderen', 'journey.settings.deleteJourney': 'Reisverslag verwijderen', 'journey.settings.deleteMessage': '"{title}" verwijderen? Alle vermeldingen en foto\'s gaan verloren.', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index ce254be0..427a2134 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -1633,6 +1633,10 @@ const pl: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Wysyłanie testowego Ntfy nie powiodło się', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Admin Ntfy zawsze wysyła po skonfigurowaniu tematu', 'admin.notifications.adminNotificationsHint': 'Skonfiguruj, które kanały dostarczają powiadomienia admina (np. alerty o wersjach). Webhook wysyła automatycznie, gdy ustawiony jest URL webhooka admina.', + 'admin.notifications.tripReminders.title': 'Przypomnienia o podróżach', + 'admin.notifications.tripReminders.hint': 'Wysyła powiadomienie z przypomnieniem przed rozpoczęciem podróży (wymaga ustawienia dni przypomnienia dla podróży).', + 'admin.notifications.tripReminders.enabled': 'Przypomnienia o podróżach włączone', + 'admin.notifications.tripReminders.disabled': 'Przypomnienia o podróżach wyłączone', 'admin.webhook.hint': 'Pozwól użytkownikom konfigurować własne adresy URL webhooka dla powiadomień (Discord, Slack itp.).', 'settings.notificationsDisabled': 'Powiadomienia nie są skonfigurowane.', 'settings.notificationPreferences.noChannels': 'Brak skonfigurowanych kanałów powiadomień. Poproś administratora o skonfigurowanie powiadomień e-mail lub webhook.', @@ -1866,6 +1870,8 @@ const pl: Record = { 'memories.saveRouteNotConfigured': 'Trasa zapisu nie jest skonfigurowana dla tego dostawcy', 'memories.testRouteNotConfigured': 'Trasa testowa nie jest skonfigurowana dla tego dostawcy', 'memories.fillRequiredFields': 'Proszę wypełnić wszystkie wymagane pola', + 'journey.search.placeholder': 'Szukaj podróży…', + 'journey.search.noResults': 'Brak podróży pasujących do „{query}"', 'journey.title': 'Dziennik podróży', 'journey.subtitle': 'Dokumentuj swoje podróże na bieżąco', 'journey.new': 'Nowy dziennik podróży', @@ -1887,6 +1893,7 @@ const pl: Record = { 'journey.status.active': 'Aktywny', 'journey.status.completed': 'Zakończony', 'journey.status.upcoming': 'Nadchodzący', + 'journey.status.archived': 'Zarchiwizowano', 'journey.checkin.add': 'Zamelduj się', 'journey.checkin.namePlaceholder': 'Nazwa miejsca', 'journey.checkin.notesPlaceholder': 'Notatki (opcjonalnie)', @@ -2040,6 +2047,11 @@ const pl: Record = { 'journey.settings.name': 'Nazwa', 'journey.settings.subtitle': 'Podtytuł', 'journey.settings.subtitlePlaceholder': 'np. Tajlandia, Wietnam i Kambodża', + 'journey.settings.endJourney': 'Archiwizuj podróż', + 'journey.settings.reopenJourney': 'Przywróć podróż', + 'journey.settings.archived': 'Podróż zarchiwizowana', + 'journey.settings.reopened': 'Podróż wznowiona', + 'journey.settings.endDescription': 'Ukrywa odznakę Na żywo. Możesz wznowić w dowolnym momencie.', 'journey.settings.delete': 'Usuń', 'journey.settings.deleteJourney': 'Usuń dziennik podróży', 'journey.settings.deleteMessage': 'Usunąć „{title}"? Wszystkie wpisy i zdjęcia zostaną utracone.', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 88668155..0bf70d93 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -1826,6 +1826,10 @@ const ru: Record = { 'admin.notifications.adminNtfyPanel.testFailed': 'Ошибка отправки тестового Ntfy', 'admin.notifications.adminNtfyPanel.alwaysOnHint': 'Ntfy администратора всегда отправляется при наличии настроенной темы', 'admin.notifications.adminNotificationsHint': 'Настройте, какие каналы доставляют уведомления администратора (например, оповещения о версиях). Вебхук отправляется автоматически, если задан URL вебхука администратора.', + 'admin.notifications.tripReminders.title': 'Напоминания о поездках', + 'admin.notifications.tripReminders.hint': 'Отправляет напоминание перед началом поездки (необходимо указать дни напоминания в параметрах поездки).', + 'admin.notifications.tripReminders.enabled': 'Напоминания о поездках включены', + 'admin.notifications.tripReminders.disabled': 'Напоминания о поездках отключены', 'admin.tabs.notifications': 'Уведомления', 'notifications.versionAvailable.title': 'Доступно обновление', 'notifications.versionAvailable.text': 'TREK {version} теперь доступен.', @@ -1873,6 +1877,8 @@ const ru: Record = { 'memories.saveRouteNotConfigured': 'Маршрут сохранения не настроен для этого провайдера', 'memories.testRouteNotConfigured': 'Маршрут тестирования не настроен для этого провайдера', 'memories.fillRequiredFields': 'Пожалуйста, заполните все обязательные поля', + 'journey.search.placeholder': 'Поиск путешествий…', + 'journey.search.noResults': 'Путешествий по запросу «{query}» не найдено', 'journey.title': 'Путешествие', 'journey.subtitle': 'Отслеживайте свои путешествия в реальном времени', 'journey.new': 'Новое путешествие', @@ -1894,6 +1900,7 @@ const ru: Record = { 'journey.status.active': 'Активно', 'journey.status.completed': 'Завершено', 'journey.status.upcoming': 'Предстоящее', + 'journey.status.archived': 'В архиве', 'journey.checkin.add': 'Отметиться', 'journey.checkin.namePlaceholder': 'Название места', 'journey.checkin.notesPlaceholder': 'Заметки (необязательно)', @@ -2047,6 +2054,11 @@ const ru: Record = { 'journey.settings.name': 'Название', 'journey.settings.subtitle': 'Подзаголовок', 'journey.settings.subtitlePlaceholder': 'напр. Таиланд, Вьетнам и Камбоджа', + 'journey.settings.endJourney': 'Архивировать путешествие', + 'journey.settings.reopenJourney': 'Восстановить путешествие', + 'journey.settings.archived': 'Путешествие архивировано', + 'journey.settings.reopened': 'Путешествие возобновлено', + 'journey.settings.endDescription': 'Скрывает значок «В эфире». Вы можете возобновить в любое время.', 'journey.settings.delete': 'Удалить', 'journey.settings.deleteJourney': 'Удалить путешествие', 'journey.settings.deleteMessage': 'Удалить «{title}»? Все записи и фото будут потеряны.', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index d2dae779..60aacb85 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -1826,6 +1826,10 @@ const zh: Record = { 'admin.notifications.adminNtfyPanel.testFailed': '测试 Ntfy 失败', 'admin.notifications.adminNtfyPanel.alwaysOnHint': '配置主题后管理员 Ntfy 始终触发', 'admin.notifications.adminNotificationsHint': '配置哪些渠道发送管理员通知(如版本更新提醒)。设置管理员 Webhook URL 后,Webhook 将自动触发。', + 'admin.notifications.tripReminders.title': '行程提醒', + 'admin.notifications.tripReminders.hint': '在行程开始前发送提醒通知(需要在行程中设置提醒天数)。', + 'admin.notifications.tripReminders.enabled': '行程提醒已启用', + 'admin.notifications.tripReminders.disabled': '行程提醒已禁用', 'admin.tabs.notifications': '通知', 'notifications.versionAvailable.title': '有可用更新', 'notifications.versionAvailable.text': 'TREK {version} 现已可用。', @@ -1873,6 +1877,8 @@ const zh: Record = { 'memories.saveRouteNotConfigured': '此提供商未配置保存路由', 'memories.testRouteNotConfigured': '此提供商未配置测试路由', 'memories.fillRequiredFields': '请填写所有必填字段', + 'journey.search.placeholder': '搜索旅程…', + 'journey.search.noResults': '没有与"{query}"匹配的旅程', 'journey.title': '旅程', 'journey.subtitle': '实时记录你的旅行', 'journey.new': '新建旅程', @@ -1894,6 +1900,7 @@ const zh: Record = { 'journey.status.active': '进行中', 'journey.status.completed': '已完成', 'journey.status.upcoming': '即将开始', + 'journey.status.archived': '已归档', 'journey.checkin.add': '签到', 'journey.checkin.namePlaceholder': '地点名称', 'journey.checkin.notesPlaceholder': '备注(可选)', @@ -2047,6 +2054,11 @@ const zh: Record = { 'journey.settings.name': '名称', 'journey.settings.subtitle': '副标题', 'journey.settings.subtitlePlaceholder': '例如 泰国、越南和柬埔寨', + 'journey.settings.endJourney': '归档旅程', + 'journey.settings.reopenJourney': '恢复旅程', + 'journey.settings.archived': '旅程已归档', + 'journey.settings.reopened': '旅程已重新开启', + 'journey.settings.endDescription': '隐藏直播标记。您可以随时重新开启。', 'journey.settings.delete': '删除', 'journey.settings.deleteJourney': '删除旅程', 'journey.settings.deleteMessage': '删除"{title}"?所有条目和照片将丢失。', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index e656e804..50a0f6b1 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -251,6 +251,10 @@ const zhTw: Record = { 'admin.notifications.adminNtfyPanel.testFailed': '測試 Ntfy 失敗', 'admin.notifications.adminNtfyPanel.alwaysOnHint': '設定主題後管理員 Ntfy 始終觸發', 'admin.notifications.adminNotificationsHint': '配置哪些渠道傳遞僅管理員通知(例如版本提醒)。', + 'admin.notifications.tripReminders.title': '行程提醒', + 'admin.notifications.tripReminders.hint': '在行程開始前發送提醒通知(需要在行程中設定提醒天數)。', + 'admin.notifications.tripReminders.enabled': '行程提醒已啟用', + 'admin.notifications.tripReminders.disabled': '行程提醒已停用', 'admin.smtp.title': '郵件與通知', 'admin.smtp.hint': '用於傳送電子郵件通知的 SMTP 配置。', 'admin.smtp.testButton': '傳送測試郵件', @@ -1833,6 +1837,8 @@ const zhTw: Record = { 'memories.saveRouteNotConfigured': '此提供商未設定儲存路由', 'memories.testRouteNotConfigured': '此提供商未設定測試路由', 'memories.fillRequiredFields': '請填寫所有必填欄位', + 'journey.search.placeholder': '搜尋旅程…', + 'journey.search.noResults': '沒有符合「{query}」的旅程', 'journey.title': '旅程', 'journey.subtitle': '即時記錄你的旅行', 'journey.new': '新建旅程', @@ -1854,6 +1860,7 @@ const zhTw: Record = { 'journey.status.active': '進行中', 'journey.status.completed': '已完成', 'journey.status.upcoming': '即將開始', + 'journey.status.archived': '已封存', 'journey.checkin.add': '打卡', 'journey.checkin.namePlaceholder': '地點名稱', 'journey.checkin.notesPlaceholder': '備註(可選)', @@ -2007,6 +2014,11 @@ const zhTw: Record = { 'journey.settings.name': '名稱', 'journey.settings.subtitle': '副標題', 'journey.settings.subtitlePlaceholder': '例如 泰國、越南和柬埔寨', + 'journey.settings.endJourney': '封存旅程', + 'journey.settings.reopenJourney': '還原旅程', + 'journey.settings.archived': '旅程已封存', + 'journey.settings.reopened': '旅程已重新開啟', + 'journey.settings.endDescription': '隱藏直播標記。您可以隨時重新開啟。', 'journey.settings.delete': '刪除', 'journey.settings.deleteJourney': '刪除旅程', 'journey.settings.deleteMessage': '刪除「{title}」?所有條目和照片將遺失。', diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index ce71e530..d758082a 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -1180,6 +1180,7 @@ export default function AdminPage(): React.ReactElement { const emailActive = activeChans.includes('email') const webhookActive = activeChans.includes('webhook') const ntfyActive = activeChans.includes('ntfy') + const tripRemindersActive = smtpValues.notify_trip_reminder !== 'false' const setChannels = async (email: boolean, webhook: boolean, ntfy: boolean) => { const chans = [email && 'email', webhook && 'webhook', ntfy && 'ntfy'].filter(Boolean).join(',') || 'none' @@ -1338,6 +1339,37 @@ export default function AdminPage(): React.ReactElement { + {/* Trip Reminders Toggle */} +
+
+
+

{t('admin.notifications.tripReminders.title')}

+

{t('admin.notifications.tripReminders.hint')}

+
+ +
+
+ {/* Admin Webhook Panel */}
diff --git a/client/src/pages/JourneyDetailPage.test.tsx b/client/src/pages/JourneyDetailPage.test.tsx index 4b7aad9d..d3b005cd 100644 --- a/client/src/pages/JourneyDetailPage.test.tsx +++ b/client/src/pages/JourneyDetailPage.test.tsx @@ -176,7 +176,7 @@ const mockJourneyDetail = { avatar: null, }, ], - stats: { entries: 2, photos: 1, cities: 2 }, + stats: { entries: 2, photos: 1, places: 2 }, }; // ── MSW Handlers ───────────────────────────────────────────────────────────── @@ -362,12 +362,12 @@ describe('JourneyDetailPage', () => { expect(screen.getAllByText('Days').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('Entries').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('Photos').length).toBeGreaterThanOrEqual(1); - expect(screen.getAllByText('Cities').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('Places').length).toBeGreaterThanOrEqual(1); }); it('renders stat values', async () => { await renderAndWait(); - // stats.entries = 2, stats.photos = 1, stats.cities = 2 + // stats.entries = 2, stats.photos = 1, stats.places = 2 // Entries count appears in hero and sidebar const twos = screen.getAllByText('2'); expect(twos.length).toBeGreaterThanOrEqual(1); @@ -474,7 +474,7 @@ describe('JourneyDetailPage', () => { // ── FE-PAGE-JOURNEYDETAIL-018 ────────────────────────────────────────── describe('FE-PAGE-JOURNEYDETAIL-018: Empty state when no entries', () => { it('shows "No entries yet" when journey has no entries', async () => { - setupDefaultHandlers({ entries: [], stats: { entries: 0, photos: 0, cities: 0 } }); + setupDefaultHandlers({ entries: [], stats: { entries: 0, photos: 0, places: 0 } }); render(); @@ -484,7 +484,7 @@ describe('JourneyDetailPage', () => { }); it('shows hint text to add a trip', async () => { - setupDefaultHandlers({ entries: [], stats: { entries: 0, photos: 0, cities: 0 } }); + setupDefaultHandlers({ entries: [], stats: { entries: 0, photos: 0, places: 0 } }); render(); @@ -567,7 +567,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [multiPhotoEntry, mockJourneyDetail.entries[1]], - stats: { entries: 2, photos: 3, cities: 2 }, + stats: { entries: 2, photos: 3, places: 2 }, }); render(); @@ -610,7 +610,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [...mockJourneyDetail.entries, skeletonEntry], - stats: { entries: 3, photos: 1, cities: 3 }, + stats: { entries: 3, photos: 1, places: 3 }, }); render(); @@ -650,7 +650,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [...mockJourneyDetail.entries, checkinEntry], - stats: { entries: 3, photos: 1, cities: 2 }, + stats: { entries: 3, photos: 1, places: 2 }, }); render(); @@ -707,15 +707,26 @@ describe('JourneyDetailPage', () => { // ── FE-PAGE-JOURNEYDETAIL-030 ────────────────────────────────────────── describe('FE-PAGE-JOURNEYDETAIL-030: Active status badge shows Live indicator', () => { - it('renders a "Live" badge for active journeys', async () => { + it('renders a "Live" badge when linked trip spans today', async () => { + setupDefaultHandlers({ + trips: [{ trip_id: 5, added_at: now, title: 'Current Trip', start_date: '2020-01-01', end_date: '2099-12-31', cover_image: null, currency: 'EUR', place_count: 8 }], + }); await renderAndWait(); expect(screen.getByText('Live')).toBeInTheDocument(); }); + + it('does not render "Live" badge when linked trip is in the past', async () => { + await renderAndWait(); + expect(screen.queryByText('Live')).not.toBeInTheDocument(); + }); }); // ── FE-PAGE-JOURNEYDETAIL-031 ────────────────────────────────────────── describe('FE-PAGE-JOURNEYDETAIL-031: Synced with Trips badge renders', () => { - it('renders the "Synced with Trips" text in the hero', async () => { + it('renders the "Synced with Trips" text in the hero for live journeys', async () => { + setupDefaultHandlers({ + trips: [{ trip_id: 5, added_at: now, title: 'Current Trip', start_date: '2020-01-01', end_date: '2099-12-31', cover_image: null, currency: 'EUR', place_count: 8 }], + }); await renderAndWait(); expect(screen.getByText('Synced with Trips')).toBeInTheDocument(); }); @@ -741,7 +752,7 @@ describe('JourneyDetailPage', () => { it('shows the place count in the sidebar map', async () => { await renderAndWait(); // The sidebar map shows "N Places" text - expect(screen.getByText(/Places/)).toBeInTheDocument(); + expect(screen.getAllByText(/Places/).length).toBeGreaterThanOrEqual(1); }); }); @@ -1717,7 +1728,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [emptyEntry], - stats: { entries: 1, photos: 0, cities: 1 }, + stats: { entries: 1, photos: 0, places: 1 }, }); render(); @@ -1930,7 +1941,7 @@ describe('JourneyDetailPage', () => { { ...mockJourneyDetail.entries[0], id: 10, entry_date: '2026-03-15' }, { ...mockJourneyDetail.entries[1], id: 11, entry_date: '2026-03-15', location_lat: 41.95, location_lng: 12.55 }, ]; - setupDefaultHandlers({ entries: twoOnSameDay, stats: { entries: 2, photos: 1, cities: 2 } }); + setupDefaultHandlers({ entries: twoOnSameDay, stats: { entries: 2, photos: 1, places: 2 } }); render(); await waitFor(() => { @@ -2005,7 +2016,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [immichEntry, mockJourneyDetail.entries[1]], - stats: { entries: 2, photos: 1, cities: 2 }, + stats: { entries: 2, photos: 1, places: 2 }, }); render(); @@ -2039,7 +2050,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [synologyEntry, mockJourneyDetail.entries[1]], - stats: { entries: 2, photos: 1, cities: 2 }, + stats: { entries: 2, photos: 1, places: 2 }, }); render(); @@ -2636,7 +2647,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [multiPhotoEntry, mockJourneyDetail.entries[1]], - stats: { entries: 2, photos: 5, cities: 2 }, + stats: { entries: 2, photos: 5, places: 2 }, }); render(); @@ -2661,7 +2672,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [twoPhotoEntry, mockJourneyDetail.entries[1]], - stats: { entries: 2, photos: 2, cities: 2 }, + stats: { entries: 2, photos: 2, places: 2 }, }); render(); @@ -3045,7 +3056,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [mockJourneyDetail.entries[0], noLocEntry], - stats: { entries: 2, photos: 1, cities: 1 }, + stats: { entries: 2, photos: 1, places: 1 }, }); render(); @@ -3528,7 +3539,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [entryWithMultiPhotos, mockJourneyDetail.entries[1]], - stats: { entries: 2, photos: 2, cities: 2 }, + stats: { entries: 2, photos: 2, places: 2 }, }); server.use( @@ -3620,7 +3631,7 @@ describe('JourneyDetailPage', () => { }; setupDefaultHandlers({ entries: [mockJourneyDetail.entries[0], noTitleEntry], - stats: { entries: 2, photos: 1, cities: 2 }, + stats: { entries: 2, photos: 1, places: 2 }, }); render(); diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index a2004fdf..f7002877 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -24,6 +24,7 @@ import MobileMapTimeline from '../components/Journey/MobileMapTimeline' import MobileEntryView from '../components/Journey/MobileEntryView' import { useIsMobile } from '../hooks/useIsMobile' import type { JourneyEntry, JourneyPhoto, JourneyDetail } from '../store/journeyStore' +import { computeJourneyLifecycle } from '../utils/journeyLifecycle' const GRADIENTS = [ 'linear-gradient(135deg, #0F172A 0%, #6366F1 45%, #EC4899 100%)', @@ -207,6 +208,14 @@ export default function JourneyDetailPage() { const dayGroups = groupByDate(timelineEntries) const sortedDates = [...dayGroups.keys()].sort() + const tripDateMin = current.trips.length + ? current.trips.reduce((min: string, t: any) => t.start_date && (!min || t.start_date < min) ? t.start_date : min, '') + : null + const tripDateMax = current.trips.length + ? current.trips.reduce((max: string, t: any) => t.end_date && (!max || t.end_date > max) ? t.end_date : max, '') + : null + const lifecycle = computeJourneyLifecycle(current.status, tripDateMin || null, tripDateMax || null) + const showMobileCombined = isMobile && view === 'timeline' return ( @@ -283,16 +292,28 @@ export default function JourneyDetailPage() {
{/* Desktop: badges */}
- {current.status === 'active' && ( + {lifecycle === 'live' && (
- Live + {t('journey.frontpage.live')} +
+ )} + {lifecycle !== 'archived' && current.trips.length > 0 && ( +
+ + {t('journey.detail.syncedWithTrips')} +
+ )} + {lifecycle !== 'live' && lifecycle !== 'archived' && ( +
+ {t(`journey.status.${lifecycle === 'upcoming' ? 'upcoming' : lifecycle === 'draft' ? 'draft' : 'completed'}`)} +
+ )} + {lifecycle === 'archived' && ( +
+ {t('journey.status.archived')}
)} -
- - {t('journey.detail.syncedWithTrips')} -
{/* Mobile: back button on the left */} + + {/* Header — mobile */} +
+
+ + +
+ {searchOpen && ( + setSearchQuery(e.target.value)} + onKeyDown={e => { if (e.key === 'Escape') { setSearchQuery(''); setSearchOpen(false) } }} + placeholder={t('journey.search.placeholder')} + className="w-full px-3.5 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-xl text-[14px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white focus:border-zinc-400 focus:outline-none" + /> + )}
{/* Header — desktop */} @@ -117,8 +157,24 @@ export default function JourneyPage() {

{t("journey.frontpage.subtitle")}

-