- Send test notifications to yourself, all admins, or trip members. These use test i18n keys. -
- - {/* Quick-fire buttons */} + {/* ── Type Testing ─────────────────────────────────────────────────── */}+ Test how each in-app notification type renders, sent to yourself. +
+ Fires each trip event to all members of the selected trip (excluding yourself). +
++ Fires each user event to the selected recipient. +
++ Fires to all admin users. +
+Loading…
+ + const visibleChannels = (['inapp', 'email', 'webhook'] as const).filter(ch => { + if (!matrix.available_channels[ch]) return false + return matrix.event_types.some((evt: string) => matrix.implemented_combos[evt]?.includes(ch)) + }) + + const toggle = async (eventType: string, channel: string) => { + const current = matrix.preferences[eventType]?.[channel] ?? true + const updated = { ...matrix.preferences, [eventType]: { ...matrix.preferences[eventType], [channel]: !current } } + setMatrix((m: any) => m ? { ...m, preferences: updated } : m) + setSaving(true) + try { + await adminApi.updateNotificationPreferences(updated) + } catch { + setMatrix((m: any) => m ? { ...m, preferences: matrix.preferences } : m) + toast.error(t('common.error')) + } finally { + setSaving(false) + } + } + + if (matrix.event_types.length === 0) { + return ( +{t('settings.notificationPreferences.noChannels')}
+{t('admin.notifications.adminNotificationsHint')}
+Saving…
} + {/* Header row */} +{t('admin.notifications.hint')}
-{t('admin.notifications.events')}
- {!configValid && ( -{t('admin.notifications.configureFirst')}
- )} -{t('admin.notifications.eventsHint')}
- {[ - { key: 'notify_trip_invite', label: t('settings.notifyTripInvite') }, - { key: 'notify_booking_change', label: t('settings.notifyBookingChange') }, - { key: 'notify_trip_reminder', label: t('settings.notifyTripReminder') }, - { key: 'notify_vacay_invite', label: t('settings.notifyVacayInvite') }, - { key: 'notify_photos_shared', label: t('settings.notifyPhotosShared') }, - { key: 'notify_collab_message', label: t('settings.notifyCollabMessage') }, - { key: 'notify_packing_tagged', label: t('settings.notifyPackingTagged') }, - ].map(opt => { - const isOn = (smtpValues[opt.key] ?? 'true') !== 'false' - return ( -{t('admin.smtp.hint')}
- {smtpLoaded && [ - { key: 'smtp_host', label: 'SMTP Host', placeholder: 'mail.example.com' }, - { key: 'smtp_port', label: 'SMTP Port', placeholder: '587' }, - { key: 'smtp_user', label: 'SMTP User', placeholder: 'trek@example.com' }, - { key: 'smtp_pass', label: 'SMTP Password', placeholder: '••••••••', type: 'password' }, - { key: 'smtp_from', label: 'From Address', placeholder: 'trek@example.com' }, - ].map(field => ( -Enable for self-signed certificates on local mail servers
-{t('admin.webhook.hint')}
-TREK will POST JSON with event, title, body, and timestamp to this URL.
-{t('admin.smtp.hint')}
+Enable for self-signed certificates on local mail servers
+{t('admin.webhook.hint')}
+{t('admin.notifications.inappPanel.hint')}
+{t('admin.notifications.adminWebhookPanel.hint')}
+Loading…
+ + // Which channels are both available AND have at least one implemented event + const visibleChannels = (['email', 'webhook', 'inapp'] as const).filter(ch => { + if (!matrix.available_channels[ch]) return false + return matrix.event_types.some(evt => matrix.implemented_combos[evt]?.includes(ch)) + }) + + if (visibleChannels.length === 0) { return (- {t('settings.notificationsDisabled')} + {t('settings.notificationPreferences.noChannels')}
) } - const channelLabel = notifChannel === 'email' - ? (t('admin.notifications.email') || 'Email (SMTP)') - : (t('admin.notifications.webhook') || 'Webhook') + const toggle = async (eventType: string, channel: string) => { + const current = matrix.preferences[eventType]?.[channel] ?? true + const updated = { + ...matrix.preferences, + [eventType]: { ...matrix.preferences[eventType], [channel]: !current }, + } + setMatrix(m => m ? { ...m, preferences: updated } : m) + setSaving(true) + try { + await notificationsApi.updatePreferences(updated) + } catch { + // Revert on failure + setMatrix(m => m ? { ...m, preferences: matrix.preferences } : m) + } finally { + setSaving(false) + } + } + + const saveWebhookUrl = async () => { + setWebhookSaving(true) + try { + await settingsApi.set('webhook_url', webhookUrl) + toast.success(t('settings.webhookUrl.saved')) + } catch { + toast.error(t('common.error')) + } finally { + setWebhookSaving(false) + } + } + + const testWebhookUrl = async () => { + if (!webhookUrl) return + setWebhookTesting(true) + try { + const result = await notificationsApi.testWebhook(webhookUrl) + if (result.success) toast.success(t('settings.webhookUrl.testSuccess')) + else toast.error(result.error || t('settings.webhookUrl.testFailed')) + } catch { + toast.error(t('settings.webhookUrl.testFailed')) + } finally { + setWebhookTesting(false) + } + } return ( -Saving…
} + {/* Webhook URL configuration */} + {matrix.available_channels.webhook && ( +{t('settings.webhookUrl.hint')}
+- {t('settings.notificationsManagedByAdmin')} -
+ {/* Event rows */} + {matrix.event_types.map(eventType => { + const implementedForEvent = matrix.implemented_combos[eventType] ?? [] + const relevantChannels = visibleChannels.filter(ch => implementedForEvent.includes(ch)) + if (relevantChannels.length === 0) return null + return ( +