import { useState, useEffect } from 'react'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; import { X, Save, Loader2, Send, CheckCircle, XCircle } from 'lucide-react'; import { api } from '../api/client'; import type { NotificationProvider, NotificationProviderCreate, NotificationProviderUpdate, ProviderType } from '../api/client'; import { Button } from './Button'; import { Toggle } from './Toggle'; interface AddNotificationModalProps { provider?: NotificationProvider | null; onClose: () => void; } const PROVIDER_OPTIONS: { value: ProviderType; label: string; description: string }[] = [ { value: 'email', label: 'Email', description: 'SMTP email notifications' }, { value: 'telegram', label: 'Telegram', description: 'Notifications via Telegram bot' }, { value: 'discord', label: 'Discord', description: 'Send to Discord channel via webhook' }, { value: 'ntfy', label: 'ntfy', description: 'Free, self-hostable push notifications' }, { value: 'pushover', label: 'Pushover', description: 'Simple, reliable push notifications' }, { value: 'callmebot', label: 'CallMeBot/WhatsApp', description: 'Free WhatsApp notifications via CallMeBot' }, { value: 'webhook', label: 'Webhook', description: 'Generic HTTP POST to any URL' }, ]; export function AddNotificationModal({ provider, onClose }: AddNotificationModalProps) { const queryClient = useQueryClient(); const isEditing = !!provider; const [name, setName] = useState(provider?.name || ''); const [providerType, setProviderType] = useState(provider?.provider_type || 'email'); const [printerId, setPrinterId] = useState(provider?.printer_id || null); const [quietHoursEnabled, setQuietHoursEnabled] = useState(provider?.quiet_hours_enabled || false); const [quietHoursStart, setQuietHoursStart] = useState(provider?.quiet_hours_start || '22:00'); const [quietHoursEnd, setQuietHoursEnd] = useState(provider?.quiet_hours_end || '07:00'); // Daily digest const [dailyDigestEnabled, setDailyDigestEnabled] = useState(provider?.daily_digest_enabled || false); const [dailyDigestTime, setDailyDigestTime] = useState(provider?.daily_digest_time || '08:00'); // Event toggles const [onPrintStart, setOnPrintStart] = useState(provider?.on_print_start ?? false); const [onPrintComplete, setOnPrintComplete] = useState(provider?.on_print_complete ?? true); const [onPrintFailed, setOnPrintFailed] = useState(provider?.on_print_failed ?? true); const [onPrintStopped, setOnPrintStopped] = useState(provider?.on_print_stopped ?? true); const [onPrintProgress, setOnPrintProgress] = useState(provider?.on_print_progress ?? false); const [onPrinterOffline, setOnPrinterOffline] = useState(provider?.on_printer_offline ?? false); const [onPrinterError, setOnPrinterError] = useState(provider?.on_printer_error ?? false); const [onFilamentLow, setOnFilamentLow] = useState(provider?.on_filament_low ?? false); const [onMaintenanceDue, setOnMaintenanceDue] = useState(provider?.on_maintenance_due ?? false); // Provider-specific config const [config, setConfig] = useState>( provider?.config ? Object.fromEntries(Object.entries(provider.config).map(([k, v]) => [k, String(v)])) : {} ); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const [error, setError] = useState(null); // Fetch printers for linking const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters, }); // Close on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); // Test configuration mutation const testMutation = useMutation({ mutationFn: () => api.testNotificationConfig({ provider_type: providerType, config }), onSuccess: (result) => { setTestResult(result); setError(null); }, onError: (err: Error) => { setTestResult({ success: false, message: err.message }); }, }); // Create mutation const createMutation = useMutation({ mutationFn: (data: NotificationProviderCreate) => api.createNotificationProvider(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notification-providers'] }); onClose(); }, onError: (err: Error) => { setError(err.message); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: (data: NotificationProviderUpdate) => api.updateNotificationProvider(provider!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notification-providers'] }); onClose(); }, onError: (err: Error) => { setError(err.message); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!name.trim()) { setError('Name is required'); return; } // Validate provider-specific config const requiredFields = getRequiredFields(providerType); for (const field of requiredFields) { if (!config[field.key]?.trim()) { setError(`${field.label} is required`); return; } } const data = { name: name.trim(), provider_type: providerType, config, printer_id: printerId, quiet_hours_enabled: quietHoursEnabled, quiet_hours_start: quietHoursEnabled ? quietHoursStart : null, quiet_hours_end: quietHoursEnabled ? quietHoursEnd : null, // Daily digest daily_digest_enabled: dailyDigestEnabled, daily_digest_time: dailyDigestEnabled ? dailyDigestTime : null, // Event toggles on_print_start: onPrintStart, on_print_complete: onPrintComplete, on_print_failed: onPrintFailed, on_print_stopped: onPrintStopped, on_print_progress: onPrintProgress, on_printer_offline: onPrinterOffline, on_printer_error: onPrinterError, on_filament_low: onFilamentLow, on_maintenance_due: onMaintenanceDue, }; if (isEditing) { updateMutation.mutate(data); } else { createMutation.mutate(data); } }; const isPending = createMutation.isPending || updateMutation.isPending; // Get config fields for each provider type const getConfigFields = (type: ProviderType) => { switch (type) { case 'callmebot': return [ { key: 'phone', label: 'Phone Number', placeholder: '+1234567890', type: 'text', required: true }, { key: 'apikey', label: 'API Key', placeholder: 'Your CallMeBot API key', type: 'text', required: true }, ]; case 'ntfy': return [ { key: 'server', label: 'Server URL', placeholder: 'https://ntfy.sh', type: 'text', required: false }, { key: 'topic', label: 'Topic', placeholder: 'my-bambuddy', type: 'text', required: true }, { key: 'auth_token', label: 'Auth Token', placeholder: 'Optional authentication', type: 'password', required: false }, ]; case 'pushover': return [ { key: 'user_key', label: 'User Key', placeholder: 'Your Pushover user key', type: 'text', required: true }, { key: 'app_token', label: 'App Token', placeholder: 'Your Pushover app token', type: 'text', required: true }, { key: 'priority', label: 'Priority', placeholder: '0 (normal)', type: 'number', required: false }, ]; case 'telegram': return [ { key: 'bot_token', label: 'Bot Token', placeholder: 'Bot token from @BotFather', type: 'password', required: true }, { key: 'chat_id', label: 'Chat ID', placeholder: 'Your chat or group ID', type: 'text', required: true }, ]; case 'email': return [ { key: 'smtp_server', label: 'SMTP Server', placeholder: 'smtp.gmail.com', type: 'text', required: true }, { key: 'smtp_port', label: 'SMTP Port', placeholder: '587', type: 'number', required: false }, { key: 'security', label: 'Security', type: 'select', required: false, options: [ { value: 'starttls', label: 'STARTTLS (Port 587)' }, { value: 'ssl', label: 'SSL/TLS (Port 465)' }, { value: 'none', label: 'None (Port 25)' }, ]}, { key: 'auth_enabled', label: 'Authentication', type: 'select', required: false, options: [ { value: 'true', label: 'Enabled' }, { value: 'false', label: 'Disabled' }, ]}, { key: 'username', label: 'Username', placeholder: 'your@email.com', type: 'text', required: false }, { key: 'password', label: 'Password', placeholder: 'App password', type: 'password', required: false }, { key: 'from_email', label: 'From Email', placeholder: 'your@email.com', type: 'text', required: true }, { key: 'to_email', label: 'To Email', placeholder: 'recipient@email.com', type: 'text', required: true }, ]; case 'discord': return [ { key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://discord.com/api/webhooks/...', type: 'text', required: true }, ]; case 'webhook': return [ { key: 'webhook_url', label: 'Webhook URL', placeholder: 'https://example.com/webhook', type: 'text', required: true }, { key: 'auth_header', label: 'Authorization', placeholder: 'Bearer token (optional)', type: 'password', required: false }, { key: 'field_title', label: 'Title Field Name', placeholder: 'title', type: 'text', required: false }, { key: 'field_message', label: 'Message Field Name', placeholder: 'message', type: 'text', required: false }, ]; default: return []; } }; const getRequiredFields = (type: ProviderType) => { return getConfigFields(type).filter(f => f.required); }; const configFields = getConfigFields(providerType); return (
e.stopPropagation()} > {/* Header */}

{isEditing ? 'Edit Notification Provider' : 'Add Notification Provider'}

{/* Form */}
{error && (
{error}
)} {/* Name */}
setName(e.target.value)} placeholder="My Notifications" className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />
{/* Provider Type */}

{PROVIDER_OPTIONS.find(o => o.value === providerType)?.description}

{/* Provider-specific configuration */}

Configuration

{configFields.map((field) => (
{field.type === 'select' && field.options ? ( ) : ( { setConfig({ ...config, [field.key]: e.target.value }); setTestResult(null); }} placeholder={field.placeholder} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> )}
))}
{/* Test Button */}
{/* Test Result */} {testResult && (
{testResult.success ? ( <> {testResult.message} ) : ( <> {testResult.message} )}
)} {/* Link to Printer */}

Only send notifications for events from this printer

{/* Quiet Hours */}
{quietHoursEnabled && (
setQuietHoursStart(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />
setQuietHoursEnd(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />
)}
{/* Daily Digest */}

Batch notifications into a single daily summary

{dailyDigestEnabled && (
setDailyDigestTime(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />

Events will be collected and sent as a single summary at this time

)}
{/* Event Toggles */}

Notification Events

{/* Print Events */}

Print Events

Start
Complete
Failed
Stopped
Progress (25%, 50%, 75%)
{/* Printer Status Events */}

Printer Status

Offline
Error
Low Filament
Maintenance
{/* Actions */}
); }