import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { Building2, DatabaseZap, RefreshCcw, Save, Search } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"; import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { HelpLink } from "@/components/HelpLink"; import { getAdminOrganizations, getZammadSettings, saveZammadSettings, syncZammadOrganizations } from "../api"; import { formatDateTime } from "../format"; import type { Organization } from "../types"; type PageSize = 10 | 20 | 50 | 100 | "all"; const pageSizeOptions: PageSize[] = [10, 20, 50, 100, "all"]; function pageSizeFromValue(value: string): PageSize { if (value === "all") { return "all"; } const parsed = Number(value); return pageSizeOptions.includes(parsed as PageSize) ? parsed as PageSize : 20; } function emptyText(value: string | null | undefined) { return value?.trim() || "leer"; } export function AdminOrganizationsPage() { const [organizations, setOrganizations] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [search, setSearch] = useState(""); const [missingAssignment, setMissingAssignment] = useState(false); const [zammadBaseUrl, setZammadBaseUrl] = useState(""); const [zammadApiKey, setZammadApiKey] = useState(""); const [hasZammadApiKey, setHasZammadApiKey] = useState(false); const [loading, setLoading] = useState(false); const [savingZammadSettings, setSavingZammadSettings] = useState(false); const [syncingOrganizations, setSyncingOrganizations] = useState(false); const loadRequestId = useRef(0); const pageCount = pageSize === "all" ? 1 : Math.max(1, Math.ceil(total / pageSize)); const safePage = Math.min(page, pageCount); const rangeStart = total === 0 ? 0 : pageSize === "all" ? 1 : (safePage - 1) * pageSize + 1; const rangeEnd = pageSize === "all" ? total : Math.min(safePage * pageSize, total); const activeFilterCount = useMemo( () => [search.trim(), missingAssignment].filter(Boolean).length, [missingAssignment, search] ); async function loadSettings() { const result = await getZammadSettings(); setZammadBaseUrl(result.settings.baseUrl); setHasZammadApiKey(result.settings.hasApiKey); } async function loadOrganizations() { const requestId = loadRequestId.current + 1; loadRequestId.current = requestId; setLoading(true); try { const result = await getAdminOrganizations({ search, page: safePage, pageSize, missingAssignment }); if (requestId === loadRequestId.current) { setOrganizations(result.organizations); setTotal(result.total); } } catch (error) { if (requestId === loadRequestId.current) { toast.error("Organisationen konnten nicht geladen werden", { description: error instanceof Error ? error.message : "Unbekannter Fehler" }); } } finally { if (requestId === loadRequestId.current) { setLoading(false); } } } useEffect(() => { void loadSettings(); }, []); useEffect(() => { setPage(1); }, [search, pageSize, missingAssignment]); useEffect(() => { void loadOrganizations(); }, [safePage, pageSize, search, missingAssignment]); async function saveZammadAccess() { setSavingZammadSettings(true); try { const result = await saveZammadSettings({ baseUrl: zammadBaseUrl, apiKey: zammadApiKey.trim() || undefined }); setZammadBaseUrl(result.settings.baseUrl); setHasZammadApiKey(result.settings.hasApiKey); setZammadApiKey(""); toast.success("Zammad-Zugang gespeichert", { description: result.settings.hasApiKey ? "URL und API-Key sind hinterlegt." : "URL wurde gespeichert." }); } catch (error) { toast.error("Zammad-Zugang konnte nicht gespeichert werden", { description: error instanceof Error ? error.message : "Unbekannter Fehler" }); } finally { setSavingZammadSettings(false); } } async function syncOrganizations(event: FormEvent) { event.preventDefault(); setSyncingOrganizations(true); try { const result = await syncZammadOrganizations({ baseUrl: zammadBaseUrl, apiKey: zammadApiKey.trim() || undefined }); toast.success("Organisationen synchronisiert", { description: `${result.attributeOptions} Attributoptionen und ${result.synced} Organisationen gespeichert, ${result.removed} Organisationen entfernt, ${result.unlinkedTickets + result.unlinkedSessions} Verknüpfung(en) gelöst.` }); setHasZammadApiKey(true); setZammadApiKey(""); await loadOrganizations(); } catch (error) { toast.error("Zammad-Sync fehlgeschlagen", { description: error instanceof Error ? error.message : "Unbekannter Fehler" }); } finally { setSyncingOrganizations(false); } } return (

Organisationsabgleich

Zammad-Organisationen synchronisieren, prüfen und nach fehlenden Zuordnungen filtern.

0 ? "secondary" : "outline"}>{activeFilterCount} Filter aktiv
Zammad-Sync Zugang speichern und Organisationen aus Zammad in die lokale Datenbank übernehmen.
setZammadBaseUrl(event.currentTarget.value)} required />
setZammadApiKey(event.currentTarget.value)} />
Organisationen {loading ? "Lädt..." : `${rangeStart}-${rangeEnd} von ${total} Organisation(en)`}
setSearch(event.currentTarget.value)} />
setPageSize(pageSizeFromValue(event.currentTarget.value))} > {pageSizeOptions.map((option) => ( {option === "all" ? "Alle" : option} ))}
Zammad-ID Organisation Zugeordnet Hotline Synchronisiert Geändert {organizations.map((organization) => ( {organization.zammad_id}
{organization.name}
{emptyText(organization.customer_owner)} {emptyText(organization.hotline_contract)} {formatDateTime(organization.synced_at)} {organization.updated_at ? formatDateTime(organization.updated_at) : "-"}
))}
{organizations.length === 0 ? (
Keine Organisationen für die aktuellen Filter.
) : null}
{pageSize !== "all" ? ( { event.preventDefault(); setPage((current) => Math.max(1, current - 1)); }} /> Seite {safePage} von {pageCount} = pageCount} className={safePage >= pageCount ? "pointer-events-none opacity-50" : undefined} onClick={(event) => { event.preventDefault(); setPage((current) => Math.min(pageCount, current + 1)); }} /> ) : null}
); }