// TODO: P2-F20 — Replace hardcoded AVAILABLE_EVENTS with backend config /** * SettingsWebhooks page — Webhook management. * * Features: * - Table listing all webhooks with URL, events, status toggle, test/edit/delete buttons * - Create/edit modal: URL, events (multi-select checkboxes), secret, retry_count, timeout * - Test button sends test payload, shows result */ import React, { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import clsx from 'clsx'; import { Plus, Pencil, Trash2, AlertTriangle, Send, Webhook as WebhookIcon, CheckCircle, XCircle } from 'lucide-react'; import { fetchWebhooks, createWebhook, updateWebhook, deleteWebhook, testWebhook, useWebhooks, useCreateWebhook, useUpdateWebhook, useDeleteWebhook, useTestWebhook, type Webhook, type CreateWebhookPayload, type UpdateWebhookPayload, type WebhookTestResult, } from '@/api/webhooks'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Modal } from '@/components/ui/Modal'; import { Input } from '@/components/ui/Input'; // ─── Available Events ─────────────────────────────────────────────────────── const AVAILABLE_EVENTS = [ 'contact.created', 'contact.updated', 'contact.deleted', 'company.created', 'company.updated', 'company.deleted', 'user.created', 'user.updated', 'deal.created', 'deal.updated', 'deal.deleted', 'task.created', 'task.updated', 'task.deleted', 'note.created', 'note.updated', 'note.deleted', ]; // ─── Webhook Form Modal (shared for create & edit) ─────────────────────────── interface WebhookFormModalProps { open: boolean; onClose: () => void; webhook?: Webhook | null; onSubmit: (data: CreateWebhookPayload | UpdateWebhookPayload) => void; isSubmitting: boolean; error?: string | null; } function WebhookFormModal({ open, onClose, webhook, onSubmit, isSubmitting, error }: WebhookFormModalProps) { const { t } = useTranslation(); const isEdit = !!webhook; const [url, setUrl] = useState(webhook?.url ?? ''); const [events, setEvents] = useState(webhook?.events ?? []); const [secret, setSecret] = useState(webhook?.secret ?? ''); const [retryCount, setRetryCount] = useState(webhook?.retry_count ?? 3); const [timeoutSeconds, setTimeoutSeconds] = useState(webhook?.timeout_seconds ?? 30); // Reset form when modal opens or webhook changes React.useEffect(() => { if (open) { setUrl(webhook?.url ?? ''); setEvents(webhook?.events ?? []); setSecret(webhook?.secret ?? ''); setRetryCount(webhook?.retry_count ?? 3); setTimeoutSeconds(webhook?.timeout_seconds ?? 30); } }, [open, webhook]); const toggleEvent = useCallback((event: string) => { setEvents((prev) => prev.includes(event) ? prev.filter((e) => e !== event) : [...prev, event] ); }, []); const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); const trimmedUrl = url.trim(); if (!trimmedUrl || events.length === 0) return; const data: CreateWebhookPayload | UpdateWebhookPayload = { url: trimmedUrl, events, secret: secret.trim() || null, retry_count: retryCount, timeout_seconds: timeoutSeconds, }; if (!isEdit) { (data as CreateWebhookPayload).is_active = true; } onSubmit(data); }, [url, events, secret, retryCount, timeoutSeconds, isEdit, onSubmit] ); return (
{/* URL */} setUrl(e.target.value)} required placeholder={t('webhooks.urlPlaceholder', 'https://example.com/webhook')} autoFocus data-testid="webhook-form-url" /> {/* Events multi-select */}
{AVAILABLE_EVENTS.map((event) => ( ))}
{events.length === 0 && (

{t('webhooks.eventsRequired', 'Mindestens ein Event erforderlich')}

)}
{/* Secret */} setSecret(e.target.value)} placeholder={t('webhooks.secretPlaceholder', 'HMAC-Signing Secret')} type="password" data-testid="webhook-form-secret" /> {/* Retry Count & Timeout */}
setRetryCount(parseInt(e.target.value) || 3)} type="number" min={0} max={10} data-testid="webhook-form-retry" /> setTimeoutSeconds(parseInt(e.target.value) || 30)} type="number" min={1} max={120} data-testid="webhook-form-timeout" />
{/* Error */} {error && (
{error}
)} {/* Actions */}
); } // ─── Delete Confirmation Modal ────────────────────────────────────────────── interface DeleteModalProps { open: boolean; webhook: Webhook | null; onConfirm: () => void; onCancel: () => void; isDeleting: boolean; } function DeleteModal({ open, webhook, onConfirm, onCancel, isDeleting }: DeleteModalProps) { const { t } = useTranslation(); return (

{t('webhooks.deleteConfirm', 'Möchten Sie den Webhook')}{' '} {webhook?.url}{' '} {t('webhooks.deleteConfirmEnd', 'wirklich löschen?')}

); } // ─── Test Result Modal ────────────────────────────────────────────────────── interface TestResultModalProps { open: boolean; result: WebhookTestResult | null; onClose: () => void; } function TestResultModal({ open, result, onClose }: TestResultModalProps) { const { t } = useTranslation(); if (!result) return null; return (
{result.success ?

{result.success ? t('webhooks.testSuccess', 'Webhook erfolgreich gesendet') : t('webhooks.testFailed', 'Webhook fehlgeschlagen') }

{result.status_code && (

{t('webhooks.statusCode', 'Status-Code')}: {result.status_code}

)} {result.error && (

{t('webhooks.error', 'Fehler')}: {result.error}

)}
); } // ─── Main Page Component ───────────────────────────────────────────────────── export function SettingsWebhooksPage() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { data: webhooks = [], isLoading, error } = useWebhooks(); const createMutation = useCreateWebhook(); const updateMutation = useUpdateWebhook(); const deleteMutation = useDeleteWebhook(); const testMutation = useTestWebhook(); // Modal state const [formModalOpen, setFormModalOpen] = useState(false); const [editingWebhook, setEditingWebhook] = useState(null); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [deletingWebhook, setDeletingWebhook] = useState(null); const [testResultModalOpen, setTestResultModalOpen] = useState(false); const [testResult, setTestResult] = useState(null); const [formError, setFormError] = useState(null); // Handlers const handleCreate = useCallback(() => { setEditingWebhook(null); setFormError(null); setFormModalOpen(true); }, []); const handleEdit = useCallback((webhook: Webhook) => { setEditingWebhook(webhook); setFormError(null); setFormModalOpen(true); }, []); const handleFormSubmit = useCallback( (data: CreateWebhookPayload | UpdateWebhookPayload) => { setFormError(null); if (editingWebhook) { updateMutation.mutate( { id: editingWebhook.id, data: data as UpdateWebhookPayload }, { onSuccess: () => { setFormModalOpen(false); setEditingWebhook(null); }, onError: (err: any) => { setFormError(err?.message || t('webhooks.updateError', 'Fehler beim Aktualisieren')); }, } ); } else { createMutation.mutate(data as CreateWebhookPayload, { onSuccess: () => { setFormModalOpen(false); }, onError: (err: any) => { setFormError(err?.message || t('webhooks.createError', 'Fehler beim Erstellen')); }, }); } }, [editingWebhook, createMutation, updateMutation, t] ); const handleDeleteClick = useCallback((webhook: Webhook) => { setDeletingWebhook(webhook); setDeleteModalOpen(true); }, []); const handleDeleteConfirm = useCallback(() => { if (!deletingWebhook) return; deleteMutation.mutate(deletingWebhook.id, { onSuccess: () => { setDeleteModalOpen(false); setDeletingWebhook(null); }, }); }, [deletingWebhook, deleteMutation]); const handleTest = useCallback( (webhook: Webhook) => { testMutation.mutate(webhook.id, { onSuccess: (result) => { setTestResult(result); setTestResultModalOpen(true); }, onError: (err: any) => { setTestResult({ success: false, status_code: null, error: err?.message || 'Unknown error' }); setTestResultModalOpen(true); }, }); }, [testMutation] ); const handleToggleActive = useCallback( (webhook: Webhook) => { updateMutation.mutate({ id: webhook.id, data: { is_active: !webhook.is_active }, }); }, [updateMutation] ); return (
{/* Header */}

{t('webhooks.title', 'Webhooks')}

{t('webhooks.description', 'Verwalten Sie ausgehende Webhook-Abonnements für Ereignisbenachrichtigungen')}

{/* Loading */} {isLoading && (
)} {/* Error */} {error && (
{t('webhooks.loadError', 'Fehler beim Laden der Webhooks')}
)} {/* Empty state */} {!isLoading && !error && webhooks.length === 0 && (

{t('webhooks.noWebhooks', 'Keine Webhooks')}

{t('webhooks.noWebhooksDesc', 'Erstellen Sie Ihren ersten Webhook, um Ereignisbenachrichtigungen an externe Dienste zu senden.')}

)} {/* Webhook list */} {!isLoading && !error && webhooks.length > 0 && (
{webhooks.map((webhook) => (
{webhook.events.slice(0, 5).map((event) => ( {event} ))} {webhook.events.length > 5 && ( +{webhook.events.length - 5} )}
{t('webhooks.retryCount', 'Retries')}: {webhook.retry_count} |{' '} {t('webhooks.timeout', 'Timeout')}: {webhook.timeout_seconds}s
{/* Toggle active */} {/* Test */} {/* Edit */} {/* Delete */}
))}
)} {/* Modals */} { setFormModalOpen(false); setEditingWebhook(null); setFormError(null); }} webhook={editingWebhook} onSubmit={handleFormSubmit} isSubmitting={createMutation.isPending || updateMutation.isPending} error={formError} /> { setDeleteModalOpen(false); setDeletingWebhook(null); }} isDeleting={deleteMutation.isPending} /> { setTestResultModalOpen(false); setTestResult(null); }} />
); } export default SettingsWebhooksPage;