import React, { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useDataProvider, useNotify, useGetIdentity, useTranslate, } from "ra-core"; import { useForm } from "react-hook-form"; import { generateApiKey } from "@/lib/api-key-utils"; import { Button } from "@/components/ds/ui/button"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ds/ui/card"; import { Plus, Trash2, Power, PowerOff, Pencil } from "lucide-react"; import { Badge } from "@/components/ds/ui/badge"; import { format } from "date-fns"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ds/ui/dialog"; import { Input } from "@/components/ds/ui/input"; import { Label } from "@/components/ds/ui/label"; import { Checkbox } from "@/components/ds/ui/checkbox"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ds/ui/tooltip"; import { isDemoMode } from "@/lib/demo-utils"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ds/ui/alert-dialog"; const AVAILABLE_EVENTS = [ { value: "contact.created", category: "contacts" }, { value: "contact.updated", category: "contacts" }, { value: "contact.deleted", category: "contacts" }, { value: "company.created", category: "companies" }, { value: "company.updated", category: "companies" }, { value: "company.deleted", category: "companies" }, { value: "deal.created", category: "deals" }, { value: "deal.updated", category: "deals" }, { value: "deal.deleted", category: "deals" }, { value: "deal.stage_changed", category: "deals" }, { value: "deal.won", category: "deals" }, { value: "deal.lost", category: "deals" }, { value: "task.created", category: "tasks" }, { value: "task.updated", category: "tasks" }, { value: "task.assigned", category: "tasks" }, { value: "task.completed", category: "tasks" }, { value: "task.priority_changed", category: "tasks" }, { value: "task.archived", category: "tasks" }, { value: "task.deleted", category: "tasks" }, { value: "invoice.created", category: "invoices" }, { value: "invoice.updated", category: "invoices" }, { value: "invoice.deleted", category: "invoices" }, { value: "invoice.status_changed", category: "invoices" }, { value: "invoice.sent", category: "invoices" }, ]; export const WebhooksTab = () => { const [showCreateDialog, setShowCreateDialog] = useState(false); const [webhookToEdit, setWebhookToEdit] = useState(null); const [webhookToDelete, setWebhookToDelete] = useState(null); const dataProvider = useDataProvider(); const notify = useNotify(); const queryClient = useQueryClient(); const translate = useTranslate(); const { data: webhooks, isLoading } = useQuery({ queryKey: ["webhooks"], queryFn: async () => { const { data } = await dataProvider.getList("webhooks", { pagination: { page: 1, perPage: 100 }, sort: { field: "created_at", order: "DESC" }, filter: {}, }); return data; }, }); const deleteMutation = useMutation({ mutationFn: async (id: number) => { await dataProvider.delete("webhooks", { id, previousData: { id } as any, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["webhooks"] }); notify(translate("crm.integrations.webhooks.notification.deleted")); setWebhookToDelete(null); }, onError: () => { notify( translate("crm.integrations.webhooks.notification.error_deleting"), { type: "error", }, ); }, }); const toggleMutation = useMutation({ mutationFn: async ({ id, is_active, }: { id: number; is_active: boolean; }) => { await dataProvider.update("webhooks", { id, data: { is_active }, previousData: {}, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["webhooks"] }); notify(translate("crm.integrations.webhooks.notification.updated")); }, onError: () => { notify( translate("crm.integrations.webhooks.notification.error_updating"), { type: "error", }, ); }, }); return (

{translate("crm.integrations.webhooks.description")}

{isDemoMode() && (

Creating webhooks is disabled in demo mode

)}
{isLoading ? ( {translate("crm.integrations.webhooks.loading")} ) : webhooks && webhooks.length > 0 ? (
{webhooks.map((webhook: any) => ( setWebhookToEdit(webhook)} onDelete={() => setWebhookToDelete(webhook.id)} onToggle={() => toggleMutation.mutate({ id: webhook.id, is_active: !webhook.is_active, }) } /> ))}
) : (

{translate("crm.integrations.webhooks.empty")}

)} setShowCreateDialog(false)} /> setWebhookToEdit(null)} /> setWebhookToDelete(null)} > {translate("crm.integrations.webhooks.dialog.delete_title")} {translate("crm.integrations.webhooks.dialog.delete_description")} {translate("crm.activity.cancel")} webhookToDelete && deleteMutation.mutate(webhookToDelete) } className="bg-destructive hover:bg-destructive/90" > {translate("crm.integrations.webhooks.action.delete")}
); }; const WebhookCard = ({ webhook, onEdit, onDelete, onToggle, }: { webhook: any; onEdit: () => void; onDelete: () => void; onToggle: () => void; }) => { const translate = useTranslate(); return (
{webhook.name}

{webhook.url}

{webhook.is_active ? ( {translate("crm.integrations.webhooks.status.active")} ) : ( {translate("crm.integrations.webhooks.status.inactive")} )} {webhook.events && webhook.events.slice(0, 3).map((event: string) => ( {translate(`crm.integrations.webhooks.events.${event}`, { _: event, })} ))} {webhook.events && webhook.events.length > 3 && ( {translate("crm.integrations.webhooks.status.more", { count: webhook.events.length - 3, })} )}
{isDemoMode() && (

Editing webhooks is disabled in demo mode

)}
{isDemoMode() && (

Toggling webhooks is disabled in demo mode

)}
{isDemoMode() && (

Deleting webhooks is disabled in demo mode

)}

{translate("crm.integrations.webhooks.fields.created", { date: format(new Date(webhook.created_at), "PPP"), })}

{webhook.last_triggered_at && (

{translate("crm.integrations.webhooks.fields.last_triggered", { date: format(new Date(webhook.last_triggered_at), "PPp"), })}

)} {webhook.failure_count > 0 && (

{translate("crm.integrations.webhooks.fields.failed_deliveries", { count: webhook.failure_count, })}

)}
); }; const CreateWebhookDialog = ({ open, onClose, }: { open: boolean; onClose: () => void; }) => { const dataProvider = useDataProvider(); const notify = useNotify(); const queryClient = useQueryClient(); const { identity } = useGetIdentity(); const translate = useTranslate(); const { register, handleSubmit, watch, setValue, reset } = useForm({ defaultValues: { name: "", url: "", events: [] as string[], }, }); const createMutation = useMutation({ mutationFn: async (values: any) => { // Generate a random secret for webhook signature const secret = generateApiKey(); await dataProvider.create("webhooks", { data: { name: values.name, url: values.url, events: values.events, is_active: true, secret, sales_id: identity?.id, created_by_sales_id: identity?.id, }, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["webhooks"] }); notify(translate("crm.integrations.webhooks.notification.created")); reset(); onClose(); }, onError: () => { notify( translate("crm.integrations.webhooks.notification.error_creating"), { type: "error", }, ); }, }); const toggleEvent = (event: string) => { const currentEvents = watch("events"); if (currentEvents.includes(event)) { setValue( "events", currentEvents.filter((e) => e !== event), ); } else { setValue("events", [...currentEvents, event]); } }; const handleClose = () => { reset(); onClose(); }; // Group events by category const eventsByCategory = AVAILABLE_EVENTS.reduce( (acc, event) => { if (!acc[event.category]) { acc[event.category] = []; } acc[event.category].push(event); return acc; }, {} as Record, ); return ( {translate("crm.integrations.webhooks.dialog.create_title")} {translate("crm.integrations.webhooks.dialog.create_description")}
createMutation.mutate(values))} >
{Object.entries(eventsByCategory).map(([category, events]) => (

{translate( `crm.integrations.webhooks.categories.${category}`, )}

{events.map((event) => (
toggleEvent(event.value)} />
))}
))}
); }; const EditWebhookDialog = ({ open, webhook, onClose, }: { open: boolean; webhook: any | null; onClose: () => void; }) => { const dataProvider = useDataProvider(); const notify = useNotify(); const queryClient = useQueryClient(); const translate = useTranslate(); const { register, handleSubmit, watch, setValue, reset } = useForm({ defaultValues: { name: webhook?.name || "", url: webhook?.url || "", events: webhook?.events || ([] as string[]), }, }); // Update form when webhook changes React.useEffect(() => { if (webhook) { setValue("name", webhook.name); setValue("url", webhook.url); setValue("events", webhook.events || []); } }, [webhook, setValue]); const updateMutation = useMutation({ mutationFn: async (values: any) => { await dataProvider.update("webhooks", { id: webhook.id, data: { name: values.name, url: values.url, events: values.events, }, previousData: webhook, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["webhooks"] }); notify(translate("crm.integrations.webhooks.notification.updated")); reset(); onClose(); }, onError: () => { notify( translate("crm.integrations.webhooks.notification.error_updating"), { type: "error", }, ); }, }); const toggleEvent = (event: string) => { const currentEvents = watch("events"); if (currentEvents.includes(event)) { setValue( "events", currentEvents.filter((e) => e !== event), ); } else { setValue("events", [...currentEvents, event]); } }; const handleClose = () => { reset(); onClose(); }; // Group events by category const eventsByCategory = AVAILABLE_EVENTS.reduce( (acc, event) => { if (!acc[event.category]) { acc[event.category] = []; } acc[event.category].push(event); return acc; }, {} as Record, ); if (!webhook) return null; return ( {translate("crm.integrations.webhooks.dialog.edit_title")} {translate("crm.integrations.webhooks.dialog.edit_description")}
updateMutation.mutate(values))} >
{Object.entries(eventsByCategory).map(([category, events]) => (

{translate( `crm.integrations.webhooks.categories.${category}`, )}

{events.map((event) => (
toggleEvent(event.value)} />
))}
))}
); };