import { useEffect, useRef, useState } from 'react' import { Bell, Mail, Hash, MessageSquare, Plus, Trash2, Loader2, Send, ToggleLeft, ToggleRight, ChevronDown, } from 'lucide-react' import { notificationsApi } from '@/api/notifications' import type { NotificationConfig, NotificationConfigCreate, NotificationConfigUpdate } from '@/types/notification' import { NOTIFICATION_EVENTS, CHANNEL_LABELS } from '@/types/notification' import { toast } from '@/lib/toast' import { cn } from '@/lib/utils' import { Input } from '@/components/ui/Input' type ChannelType = 'email' | 'slack_webhook' | 'teams_webhook' const CHANNEL_ICONS: Record = { email: Mail, slack_webhook: Hash, teams_webhook: MessageSquare, } function maskWebhookUrl(url: string): string { if (url.length <= 8) return url return '\u2022'.repeat(12) + url.slice(-8) } export function NotificationSettings() { const [configs, setConfigs] = useState([]) const [loading, setLoading] = useState(true) const [addingChannel, setAddingChannel] = useState(null) const [testingId, setTestingId] = useState(null) const [showDropdown, setShowDropdown] = useState(false) const [confirmDeleteId, setConfirmDeleteId] = useState(null) const dropdownRef = useRef(null) // Add form state const [newWebhookUrl, setNewWebhookUrl] = useState('') const [newEmails, setNewEmails] = useState('') const [isSaving, setIsSaving] = useState(false) useEffect(() => { loadConfigs() }, []) // Close dropdown on outside click useEffect(() => { function handleClickOutside(e: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setShowDropdown(false) } } if (showDropdown) { document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) } }, [showDropdown]) const loadConfigs = async () => { try { const data = await notificationsApi.listConfigs() setConfigs(data) } catch (err) { console.error('Failed to load notification configs:', err) toast.error('Failed to load notification settings') } finally { setLoading(false) } } const handleAddChannel = (channel: ChannelType) => { setAddingChannel(channel) setShowDropdown(false) setNewWebhookUrl('') setNewEmails('') } const handleSaveNew = async () => { if (!addingChannel) return setIsSaving(true) try { const payload: NotificationConfigCreate = { channel: addingChannel } if (addingChannel === 'email') { const emails = newEmails.split(',').map(e => e.trim()).filter(Boolean) if (emails.length === 0) { toast.error('Please enter at least one email address') setIsSaving(false) return } payload.email_addresses = emails } else { if (!newWebhookUrl.trim()) { toast.error('Please enter a webhook URL') setIsSaving(false) return } payload.webhook_url = newWebhookUrl.trim() } await notificationsApi.createConfig(payload) await loadConfigs() setAddingChannel(null) setNewWebhookUrl('') setNewEmails('') toast.success(`${CHANNEL_LABELS[addingChannel]} channel added`) } catch (err) { console.error('Failed to create notification config:', err) toast.error('Failed to add channel') } finally { setIsSaving(false) } } const handleToggleActive = async (config: NotificationConfig) => { try { const update: NotificationConfigUpdate = { is_active: !config.is_active } await notificationsApi.updateConfig(config.id, update) setConfigs(prev => prev.map(c => c.id === config.id ? { ...c, is_active: !c.is_active } : c)) toast.success(config.is_active ? 'Channel disabled' : 'Channel enabled') } catch (err) { console.error('Failed to toggle config:', err) toast.error('Failed to update channel') } } const handleToggleEvent = async (config: NotificationConfig, eventKey: string) => { const updated = { ...config.events_enabled, [eventKey]: !config.events_enabled[eventKey] } try { await notificationsApi.updateConfig(config.id, { events_enabled: updated }) setConfigs(prev => prev.map(c => c.id === config.id ? { ...c, events_enabled: updated } : c)) } catch (err) { console.error('Failed to update events:', err) toast.error('Failed to update event settings') } } const handleTest = async (configId: string) => { setTestingId(configId) try { const result = await notificationsApi.testConfig(configId) if (result.success) { toast.success(result.message || 'Test notification sent') } else { toast.error(result.message || 'Test failed') } } catch (err) { console.error('Failed to test config:', err) toast.error('Test notification failed') } finally { setTestingId(null) } } const handleDelete = async (configId: string) => { try { await notificationsApi.deleteConfig(configId) setConfigs(prev => prev.filter(c => c.id !== configId)) toast.success('Channel removed') } catch (err) { console.error('Failed to delete config:', err) toast.error('Failed to remove channel') } } return (
{/* Section header */}

Notifications

{showDropdown && (
{(Object.entries(CHANNEL_LABELS) as [ChannelType, string][]).map(([key, label]) => { const Icon = CHANNEL_ICONS[key] return ( ) })}
)}
{/* Loading */} {loading && (
)} {/* Empty state */} {!loading && configs.length === 0 && !addingChannel && (

No notification channels configured. Add a channel to receive alerts for session events.

)} {/* Channel list */} {!loading && (
{configs.map(config => { const Icon = CHANNEL_ICONS[config.channel] return (
{/* Header row */}
{CHANNEL_LABELS[config.channel]} {config.is_active ? 'Active' : 'Inactive'}
{/* Config details */}
{config.webhook_url && (
Webhook URL

{maskWebhookUrl(config.webhook_url)}

)} {config.email_addresses && config.email_addresses.length > 0 && (
Email Addresses

{config.email_addresses.join(', ')}

)}
{/* Event toggles */}
Events
{Object.entries(NOTIFICATION_EVENTS).map(([eventKey, eventLabel]) => ( ))}
{/* Action buttons */}
{confirmDeleteId === config.id ? ( ) : ( )}
) })} {/* Inline add form */} {addingChannel && (
{(() => { const Icon = CHANNEL_ICONS[addingChannel] return })()} Add {CHANNEL_LABELS[addingChannel]}
{addingChannel === 'email' ? (
setNewEmails(e.target.value)} placeholder="user@example.com, team@example.com" className="mt-1" />

Separate multiple addresses with commas

) : (
setNewWebhookUrl(e.target.value)} placeholder={ addingChannel === 'slack_webhook' ? 'https://hooks.slack.com/services/...' : 'https://outlook.office.com/webhook/...' } className="mt-1" />
)}
)}
)}
) } export default NotificationSettings