feat: Integrationen sichtbar und bedienbar machen

Eine Seite zeigt je Integration, ob sie vorgesehen, eingerichtet oder
gestoert ist, wann sie zuletzt geglueckt ist und woran sie klemmt. Der
Wartungsplaner steht sichtbar als offene Luecke darin, nicht als
Leerstelle.

Telegram-Chats werden hier gekoppelt und wieder getrennt, Meldungen je
Chat einzeln geschaltet. Aufgaben bekommen einen Schalter zum Spiegeln —
einzeln und je Aufgabe, denn wer versehentlich einen ganzen Bereich
spiegelt, schickt Kundennamen nach Hause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 23:04:33 +02:00
co-authored by Claude Opus 5
parent bc60e5f864
commit 1281c3ea96
8 changed files with 624 additions and 3 deletions
@@ -0,0 +1,139 @@
'use client'
import { useState, useTransition } from 'react'
import { useRouter } from 'next/navigation'
import { Bell, BellOff, Link2, Loader2, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
chatEntfernen,
chatMeldungenUmschalten,
kopplungAnfordern,
} from '@/server/aktionen/integrationen'
export type ChatAnzeige = {
id: string
chatId: string
bezeichnung: string | null
meldungen: boolean
erstelltAm: string
zuletztBenutzt: string | null
}
export function TelegramChats({ chats }: { chats: ChatAnzeige[] }) {
const router = useRouter()
const [laeuft, starte] = useTransition()
const [code, setCode] = useState<{ code: string; minuten: number } | null>(null)
function anfordern() {
starte(async () => {
const antwort = await kopplungAnfordern()
if (antwort.ok) {
setCode({ code: antwort.code, minuten: antwort.minuten })
} else {
toast.error(antwort.fehler)
}
})
}
function mitRueckmeldung(
tun: () => Promise<{ ok: boolean; meldung?: string; fehler?: string }>,
) {
starte(async () => {
const antwort = await tun()
if (antwort.ok) {
toast.success(antwort.meldung ?? 'Erledigt.')
router.refresh()
} else {
toast.error(antwort.fehler ?? 'Das hat nicht geklappt.')
}
})
}
return (
<div className="flex flex-col gap-4">
{chats.length === 0 ? (
<p className="text-gedaempft-vordergrund text-sm">
Noch kein Chat verbunden. Ohne Verbindung nimmt der Bot nichts an.
</p>
) : (
<ul className="divide-rand divide-y">
{chats.map((chat) => (
<li key={chat.id} className="flex items-center gap-3 py-2">
<div className="min-w-0 flex-1">
<p className="text-sm">{chat.bezeichnung ?? `Chat ${chat.chatId}`}</p>
<p className="text-gedaempft-vordergrund text-xs">
Verbunden {chat.erstelltAm}
{chat.zuletztBenutzt
? ` · zuletzt benutzt ${chat.zuletztBenutzt}`
: ' · noch nichts geschickt'}
</p>
</div>
<Button
art="leise"
groesse="quadrat"
title={
chat.meldungen ? 'Keine Meldungen mehr hierher' : 'Meldungen hierher'
}
disabled={laeuft}
onClick={() => {
const daten = new FormData()
daten.set('id', chat.id)
mitRueckmeldung(() => chatMeldungenUmschalten(daten))
}}
>
{chat.meldungen ? (
<Bell className="text-erfolg" aria-hidden />
) : (
<BellOff aria-hidden />
)}
<span className="sr-only">Meldungen umschalten</span>
</Button>
<Button
art="leise"
groesse="quadrat"
title="Chat trennen"
disabled={laeuft}
onClick={() => {
const daten = new FormData()
daten.set('id', chat.id)
mitRueckmeldung(() => chatEntfernen(daten))
}}
>
<Trash2 aria-hidden />
<span className="sr-only">Trennen</span>
</Button>
</li>
))}
</ul>
)}
{code ? (
<div className="border-rand rounded-md border p-3">
<p className="text-sm">
Schick dem Bot jetzt diesen Code — als Nachricht oder als{' '}
<code className="text-xs">/start {code.code}</code>:
</p>
<p className="my-2 font-mono text-2xl tracking-widest">{code.code}</p>
<p className="text-gedaempft-vordergrund text-xs">
Gültig für {code.minuten} Minuten und nur ein einziges Mal. Danach erscheint
der Chat hier — spätestens nach dem nächsten Abholen.
</p>
</div>
) : (
<div>
<Button art="umriss" disabled={laeuft} onClick={anfordern}>
{laeuft ? (
<Loader2 className="animate-spin" aria-hidden />
) : (
<Link2 aria-hidden />
)}
Chat verbinden
</Button>
</div>
)}
</div>
)
}