import React, { useState, useEffect } from 'react' import { Lock } from 'lucide-react' import { useTranslation } from '../../i18n' import { notificationsApi, settingsApi } from '../../api/client' import { useToast } from '../shared/Toast' import ToggleSwitch from './ToggleSwitch' import Section from './Section' interface PreferencesMatrix { preferences: Record> available_channels: { email: boolean; webhook: boolean; inapp: boolean } event_types: string[] implemented_combos: Record } const CHANNEL_LABEL_KEYS: Record = { email: 'settings.notificationPreferences.email', webhook: 'settings.notificationPreferences.webhook', inapp: 'settings.notificationPreferences.inapp', } const EVENT_LABEL_KEYS: Record = { trip_invite: 'settings.notifyTripInvite', booking_change: 'settings.notifyBookingChange', trip_reminder: 'settings.notifyTripReminder', vacay_invite: 'settings.notifyVacayInvite', photos_shared: 'settings.notifyPhotosShared', collab_message: 'settings.notifyCollabMessage', packing_tagged: 'settings.notifyPackingTagged', version_available: 'settings.notifyVersionAvailable', } export default function NotificationsTab(): React.ReactElement { const { t } = useTranslation() const toast = useToast() const [matrix, setMatrix] = useState(null) const [saving, setSaving] = useState(false) const [webhookUrl, setWebhookUrl] = useState('') const [webhookIsSet, setWebhookIsSet] = useState(false) const [webhookSaving, setWebhookSaving] = useState(false) const [webhookTesting, setWebhookTesting] = useState(false) useEffect(() => { notificationsApi.getPreferences().then((data: PreferencesMatrix) => setMatrix(data)).catch(() => {}) settingsApi.get().then((data: { settings: Record }) => { const val = (data.settings?.webhook_url as string) || '' if (val === '••••••••') { setWebhookIsSet(true) setWebhookUrl('') } else { setWebhookUrl(val) } }).catch(() => {}) }, []) const visibleChannels = matrix ? (['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)) }) : [] const toggle = async (eventType: string, channel: string) => { if (!matrix) return 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 { setMatrix(m => m ? { ...m, preferences: matrix.preferences } : m) } finally { setSaving(false) } } const saveWebhookUrl = async () => { setWebhookSaving(true) try { await settingsApi.set('webhook_url', webhookUrl) if (webhookUrl) setWebhookIsSet(true) else setWebhookIsSet(false) toast.success(t('settings.webhookUrl.saved')) } catch { toast.error(t('common.error')) } finally { setWebhookSaving(false) } } const testWebhookUrl = async () => { if (!webhookUrl && !webhookIsSet) return setWebhookTesting(true) try { const result = await notificationsApi.testWebhook(webhookUrl || undefined) 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) } } const renderContent = () => { if (!matrix) return

Loading…

if (visibleChannels.length === 0) { return (

{t('settings.notificationPreferences.noChannels')}

) } return (
{saving &&

Saving…

} {matrix.available_channels.webhook && (

{t('settings.webhookUrl.hint')}

setWebhookUrl(e.target.value)} placeholder={webhookIsSet ? '••••••••' : t('settings.webhookUrl.placeholder')} style={{ flex: 1, fontSize: 13, padding: '6px 10px', border: '1px solid var(--border-primary)', borderRadius: 6, background: 'var(--bg-primary)', color: 'var(--text-primary)' }} />
)} {/* Header row */}
'64px').join(' ')}`, gap: 4, paddingBottom: 6, marginBottom: 4, borderBottom: '1px solid var(--border-primary)' }}> {visibleChannels.map(ch => ( {t(CHANNEL_LABEL_KEYS[ch]) || ch} ))}
{/* 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 (
'64px').join(' ')}`, gap: 4, alignItems: 'center', padding: '6px 0', borderBottom: '1px solid var(--border-primary)' }}> {t(EVENT_LABEL_KEYS[eventType]) || eventType} {visibleChannels.map(ch => { if (!implementedForEvent.includes(ch)) { return } const isOn = matrix.preferences[eventType]?.[ch] ?? true return (
toggle(eventType, ch)} />
) })}
) })}
) } return (
{renderContent()}
) }