feat(ui): webhooks as cards with events in words, calendar sync without env jargon

- Webhooks: one card per connection, events by their German label
  instead of keys like "product.stock_changed", state as icon + word,
  a one-sentence explanation of what a webhook is for, retry on errors.
- Calendar sync: the group id field says where to find it; the
  "not configured" note says who sets it up instead of listing
  environment variable names.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-01 12:15:54 +02:00
1 parent a21b1665e7
commit 549a75988e
2 files changed
+55 -56

No files matched your search

@@ -87,13 +87,18 @@ function SettingsForm({ settings }: { settings: CalendarSyncSettings }) {
<Label htmlFor="sync-enabled">Synchronisation aktiv</Label>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="sync-group-id">Objekt-ID der M365-Gruppe</Label>
<Label htmlFor="sync-group-id">Kennung des Microsoft-365-Gruppenkalenders</Label>
<Input
id="sync-group-id"
value={groupId}
onChange={(event) => setGroupId(event.target.value)}
placeholder="z. B. 3fa85f64-5717-4562-b3fc-2c963f66afa6"
aria-describedby="sync-group-id-hint"
/>
<p id="sync-group-id-hint" className="text-xs text-muted-foreground">
Steht im Microsoft-Entra-Admin-Center unter Gruppen → gewünschte Gruppe →
„Objekt-ID“. Bei Unklarheit die IT fragen.
</p>
</div>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">Welche Einträge übertragen werden</p>
@@ -148,9 +153,9 @@ function StatusSection() {
<p className="text-muted-foreground">Lädt…</p>
) : !status?.configured ? (
<p className="text-muted-foreground">
Nicht konfiguriert - die SSO-Umgebungsvariablen (OIDC_ISSUER_URL/OIDC_CLIENT_ID/
OIDC_CLIENT_SECRET) fehlen, oder die App-Registrierung hat noch keine
Application-Berechtigung „Calendars.ReadWrite“.
Noch nicht eingerichtet. Das erledigt die Person, die den Server betreut: Microsoft-
Anmeldung (SSO) einrichten und der App die Berechtigung „Calendars.ReadWrite“ geben
(Details in der Betriebsanleitung, ARCHITECTURE.md Abschnitt 6.9).
</p>
) : !canRunNow ? (
<p className="text-muted-foreground">
@@ -1,24 +1,22 @@
"use client";
import { CircleCheck, CircleSlash, Webhook } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { deleteWebhook, fetchWebhooks } from "@/lib/webhooks/api";
import { WEBHOOK_EVENT_LABELS } from "@/lib/webhooks/labels";
import { errorMessage } from "@/lib/forms/error-message";
import { Badge } from "@/components/ui/badge";
import { ConfirmDialog } from "@/components/confirm-dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { EmptyState } from "@/components/empty-state";
import { QueryState } from "@/components/query-state";
import { CreateWebhookDialog } from "./create-webhook-dialog";
import { EditWebhookDialog } from "./edit-webhook-dialog";
/** One card per connection - events in words, not technical keys. */
export function WebhooksTab() {
const queryClient = useQueryClient();
const { data: webhooks, isLoading, isError } = useQuery({
const webhooksQuery = useQuery({
queryKey: ["webhooks"],
queryFn: fetchWebhooks,
});
@@ -29,61 +27,59 @@ export function WebhooksTab() {
void queryClient.invalidateQueries({ queryKey: ["webhooks"] });
toast.success("Webhook gelöscht");
},
onError: () => toast.error("Webhook konnte nicht gelöscht werden"),
onError: (error) => toast.error(errorMessage(error, "Webhook konnte nicht gelöscht werden")),
});
return (
<div className="flex flex-col gap-4">
<p className="max-w-prose text-sm text-muted-foreground">
Ein Webhook meldet einem anderen System sofort, wenn hier etwas passiert – zum Beispiel
eine neue Ernte. Nur nötig, wenn ein anderes Programm darauf wartet.
</p>
<div className="flex justify-end">
<CreateWebhookDialog />
</div>
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
{isError ? (
<p role="alert" className="text-sm text-destructive">
Webhooks konnten nicht geladen werden.
</p>
) : null}
{webhooks && webhooks.length === 0 ? (
<p className="text-sm text-muted-foreground">Noch keine Webhooks angelegt.</p>
) : null}
{webhooks && webhooks.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>URL</TableHead>
<TableHead>Ereignisse</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Aktion</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<QueryState
query={webhooksQuery}
what="Webhooks"
empty={<EmptyState icon={Webhook} title="Noch keine Webhooks angelegt" />}
>
{(webhooks) => (
<ul className="flex flex-col gap-2">
{webhooks.map((webhook) => (
<TableRow key={webhook.id}>
<TableCell className="max-w-64 truncate font-mono text-xs">{webhook.url}</TableCell>
<TableCell>
<li
key={webhook.id}
className="flex flex-col gap-3 rounded-xl border border-border/60 bg-card p-3 sm:flex-row sm:items-start"
>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<span className="break-all font-mono text-xs">{webhook.url}</span>
<span
className={
webhook.active
? "inline-flex items-center gap-1 text-xs font-medium"
: "inline-flex items-center gap-1 text-xs text-muted-foreground"
}
>
{webhook.active ? (
<CircleCheck className="size-3.5" aria-hidden />
) : (
<CircleSlash className="size-3.5" aria-hidden />
)}
{webhook.active ? "Aktiv" : "Pausiert"}
</span>
<div className="flex flex-wrap gap-1">
{webhook.events.map((event) => (
<Badge key={event} variant="secondary">
{event}
{WEBHOOK_EVENT_LABELS[event] ?? event}
</Badge>
))}
</div>
</TableCell>
<TableCell>
<Badge variant={webhook.active ? "default" : "outline"}>
{webhook.active ? "Aktiv" : "Inaktiv"}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
</div>
<div className="flex flex-wrap gap-2">
<EditWebhookDialog webhook={webhook} />
<ConfirmDialog
triggerLabel="Löschen"
triggerSize="sm"
triggerDisabled={deleteMutation.isPending}
title="Webhook löschen?"
description={`Der Webhook für „${webhook.url}“ wird endgültig gelöscht. Externe Systeme erhalten danach keine Ereignisse mehr über diese Verbindung, und der Vorgang kann nicht rückgängig gemacht werden.`}
@@ -92,13 +88,11 @@ export function WebhooksTab() {
onConfirm={() => deleteMutation.mutate(webhook.id)}
/>
</div>
</TableCell>
</TableRow>
</li>
))}
</TableBody>
</Table>
</div>
) : null}
</ul>
)}
</QueryState>
</div>
);
}