477 lines
22 KiB
TypeScript
477 lines
22 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import {
|
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import { useToast } from '@/components/ui/use-toast'
|
|
import {
|
|
Shield, Trash2, UserPlus, Loader2, Ban, CheckCircle, Clock, AlertTriangle, Copy, Mail, MailX, Upload, X,
|
|
} from 'lucide-react'
|
|
|
|
interface TenantDetail {
|
|
id: string
|
|
name: string
|
|
slug: string
|
|
description: string | null
|
|
isActive: boolean
|
|
contactEmail: string | null
|
|
contactPhone: string | null
|
|
address: string | null
|
|
plan: string
|
|
subscriptionStatus: string
|
|
trialEndsAt: string | null
|
|
subscriptionEndsAt: string | null
|
|
maxUsers: number
|
|
maxProjects: number
|
|
logoUrl: string | null
|
|
notes: string | null
|
|
createdAt: string
|
|
memberships: { id: string; role: string; user: { id: string; email: string; name: string; role: string; lastLoginAt: string | null } }[]
|
|
_count: { projects: number; memberships: number }
|
|
}
|
|
|
|
const PLANS = [
|
|
{ value: 'FREE', label: 'Free', desc: '5 Benutzer, 10 Projekte', color: 'bg-gray-100 text-gray-700' },
|
|
{ value: 'PRO', label: 'Pro', desc: 'CHF 45/Monat — Unbegrenzte Benutzer & Projekte', color: 'bg-blue-100 text-blue-700' },
|
|
]
|
|
|
|
const STATUSES = [
|
|
{ value: 'ACTIVE', label: 'Aktiv', icon: CheckCircle, color: 'text-green-600' },
|
|
{ value: 'SUSPENDED', label: 'Gesperrt', icon: Ban, color: 'text-red-600' },
|
|
{ value: 'EXPIRED', label: 'Abgelaufen', icon: AlertTriangle, color: 'text-orange-600' },
|
|
{ value: 'CANCELLED', label: 'Gekündigt', icon: Ban, color: 'text-gray-600' },
|
|
]
|
|
|
|
interface Props {
|
|
tenantId: string | null
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onUpdated: () => void
|
|
}
|
|
|
|
export function TenantDetailDialog({ tenantId, open, onOpenChange, onUpdated }: Props) {
|
|
const { toast } = useToast()
|
|
const [loading, setLoading] = useState(false)
|
|
const [tenant, setTenant] = useState<TenantDetail | null>(null)
|
|
const [tab, setTab] = useState('info')
|
|
|
|
// Editable fields
|
|
const [name, setName] = useState('')
|
|
const [description, setDescription] = useState('')
|
|
const [contactEmail, setContactEmail] = useState('')
|
|
const [contactPhone, setContactPhone] = useState('')
|
|
const [address, setAddress] = useState('')
|
|
const [plan, setPlan] = useState('FREE')
|
|
const [status, setStatus] = useState('TRIAL')
|
|
const [maxUsers, setMaxUsers] = useState(5)
|
|
const [maxProjects, setMaxProjects] = useState(10)
|
|
const [trialEndsAt, setTrialEndsAt] = useState('')
|
|
const [subscriptionEndsAt, setSubscriptionEndsAt] = useState('')
|
|
const [notes, setNotes] = useState('')
|
|
|
|
// Add member (NEW user)
|
|
const [newUserName, setNewUserName] = useState('')
|
|
const [newUserEmail, setNewUserEmail] = useState('')
|
|
const [newUserRole, setNewUserRole] = useState('OPERATOR')
|
|
const [addingUser, setAddingUser] = useState(false)
|
|
const [createdUserInfo, setCreatedUserInfo] = useState<{ email: string; tempPassword: string; emailSent: boolean } | null>(null)
|
|
|
|
// Logo
|
|
const [uploadingLogo, setUploadingLogo] = useState(false)
|
|
|
|
// Delete confirmation
|
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
const [deleteSlug, setDeleteSlug] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (tenantId && open) {
|
|
fetchTenant()
|
|
setCreatedUserInfo(null)
|
|
setConfirmDelete(false)
|
|
setDeleteSlug('')
|
|
}
|
|
}, [tenantId, open])
|
|
|
|
const fetchTenant = async () => {
|
|
if (!tenantId) return
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const t = data.tenant
|
|
setTenant(t)
|
|
setName(t.name)
|
|
setDescription(t.description || '')
|
|
setContactEmail(t.contactEmail || '')
|
|
setContactPhone(t.contactPhone || '')
|
|
setAddress(t.address || '')
|
|
setPlan(t.plan)
|
|
setStatus(t.subscriptionStatus)
|
|
setMaxUsers(t.maxUsers)
|
|
setMaxProjects(t.maxProjects)
|
|
setTrialEndsAt(t.trialEndsAt ? t.trialEndsAt.split('T')[0] : '')
|
|
setSubscriptionEndsAt(t.subscriptionEndsAt ? t.subscriptionEndsAt.split('T')[0] : '')
|
|
setNotes(t.notes || '')
|
|
}
|
|
} catch (e) {
|
|
console.error(e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
if (!tenantId) return
|
|
try {
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name, description: description || null,
|
|
contactEmail: contactEmail || null, contactPhone: contactPhone || null, address: address || null,
|
|
plan, subscriptionStatus: status, maxUsers, maxProjects,
|
|
trialEndsAt: trialEndsAt || null, subscriptionEndsAt: subscriptionEndsAt || null,
|
|
notes: notes || null,
|
|
}),
|
|
})
|
|
if (res.ok) {
|
|
toast({ title: 'Mandant aktualisiert' })
|
|
onUpdated()
|
|
fetchTenant()
|
|
} else {
|
|
const err = await res.json()
|
|
throw new Error(err.error)
|
|
}
|
|
} catch (error) {
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
|
|
}
|
|
}
|
|
|
|
const handleSuspend = async () => {
|
|
if (!tenantId) return
|
|
const newStatus = status === 'SUSPENDED' ? 'ACTIVE' : 'SUSPENDED'
|
|
try {
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ subscriptionStatus: newStatus, isActive: newStatus !== 'SUSPENDED' }),
|
|
})
|
|
if (res.ok) {
|
|
toast({ title: newStatus === 'SUSPENDED' ? 'Mandant gesperrt' : 'Mandant aktiviert' })
|
|
setStatus(newStatus)
|
|
onUpdated()
|
|
fetchTenant()
|
|
}
|
|
} catch {
|
|
toast({ title: 'Fehler', variant: 'destructive' })
|
|
}
|
|
}
|
|
|
|
const handleDeleteTenant = async () => {
|
|
if (!tenantId || deleteSlug !== tenant?.slug) return
|
|
try {
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}`, { method: 'DELETE' })
|
|
if (res.ok) {
|
|
toast({ title: 'Mandant gelöscht' })
|
|
onOpenChange(false)
|
|
onUpdated()
|
|
} else {
|
|
const err = await res.json()
|
|
throw new Error(err.error)
|
|
}
|
|
} catch (error) {
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
|
|
}
|
|
}
|
|
|
|
const handleAddMember = async () => {
|
|
if (!tenantId || !newUserName || !newUserEmail) return
|
|
setAddingUser(true)
|
|
setCreatedUserInfo(null)
|
|
try {
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}/members`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: newUserName, email: newUserEmail, role: newUserRole }),
|
|
})
|
|
const data = await res.json()
|
|
if (res.ok) {
|
|
setCreatedUserInfo({ email: newUserEmail.toLowerCase(), tempPassword: data.tempPassword, emailSent: data.emailSent })
|
|
toast({ title: 'Benutzer erstellt und hinzugefügt' })
|
|
setNewUserName('')
|
|
setNewUserEmail('')
|
|
setNewUserRole('OPERATOR')
|
|
fetchTenant()
|
|
onUpdated()
|
|
} else {
|
|
throw new Error(data.error)
|
|
}
|
|
} catch (error) {
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
|
|
} finally {
|
|
setAddingUser(false)
|
|
}
|
|
}
|
|
|
|
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (!tenantId || !e.target.files?.[0]) return
|
|
setUploadingLogo(true)
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('logo', e.target.files[0])
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}/logo`, { method: 'POST', body: formData })
|
|
const data = await res.json()
|
|
if (res.ok) {
|
|
toast({ title: 'Logo hochgeladen' })
|
|
fetchTenant()
|
|
} else {
|
|
throw new Error(data.error)
|
|
}
|
|
} catch (error) {
|
|
toast({ title: 'Fehler', description: error instanceof Error ? error.message : 'Upload fehlgeschlagen', variant: 'destructive' })
|
|
} finally {
|
|
setUploadingLogo(false)
|
|
e.target.value = ''
|
|
}
|
|
}
|
|
|
|
const handleLogoDelete = async () => {
|
|
if (!tenantId) return
|
|
try {
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}/logo`, { method: 'DELETE' })
|
|
if (res.ok) {
|
|
toast({ title: 'Logo entfernt' })
|
|
fetchTenant()
|
|
}
|
|
} catch {
|
|
toast({ title: 'Fehler', variant: 'destructive' })
|
|
}
|
|
}
|
|
|
|
const handleRemoveMember = async (membershipId: string) => {
|
|
if (!tenantId || !confirm('Benutzer wirklich entfernen? Der Benutzer und seine Daten werden gelöscht.')) return
|
|
try {
|
|
const res = await fetch(`/api/admin/tenants/${tenantId}/members?membershipId=${membershipId}&deleteUser=true`, { method: 'DELETE' })
|
|
if (res.ok) {
|
|
toast({ title: 'Benutzer entfernt' })
|
|
fetchTenant()
|
|
onUpdated()
|
|
}
|
|
} catch {
|
|
toast({ title: 'Fehler', variant: 'destructive' })
|
|
}
|
|
}
|
|
|
|
const statusInfo = STATUSES.find(s => s.value === status)
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Shield className="w-5 h-5" />
|
|
{tenant?.name || 'Mandant'}
|
|
{statusInfo && (
|
|
<span className={`text-xs px-2 py-0.5 rounded-full ${STATUSES.find(s => s.value === tenant?.subscriptionStatus)?.color || ''}`}>
|
|
{statusInfo.label}
|
|
</span>
|
|
)}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-8"><Loader2 className="w-6 h-6 animate-spin" /></div>
|
|
) : tenant ? (
|
|
<Tabs value={tab} onValueChange={setTab}>
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="info">Stammdaten</TabsTrigger>
|
|
<TabsTrigger value="members">Benutzer ({tenant.memberships.length})</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* === INFO TAB === */}
|
|
<TabsContent value="info" className="space-y-3 mt-3">
|
|
{/* Logo */}
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-16 h-16 rounded-lg border bg-muted flex items-center justify-center overflow-hidden shrink-0">
|
|
{tenant.logoUrl ? (
|
|
<img src={tenant.logoUrl.startsWith('/') ? tenant.logoUrl : `/api/admin/tenants/${tenantId}/logo/serve`} alt="Logo" className="w-full h-full object-contain" />
|
|
) : (
|
|
<Shield className="w-8 h-8 text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Logo</Label>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" className="relative" disabled={uploadingLogo}>
|
|
{uploadingLogo ? <Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" /> : <Upload className="w-3.5 h-3.5 mr-1" />}
|
|
{tenant.logoUrl ? 'Ändern' : 'Hochladen'}
|
|
<input type="file" accept="image/png,image/jpeg,image/svg+xml,image/webp" onChange={handleLogoUpload} className="absolute inset-0 opacity-0 cursor-pointer" />
|
|
</Button>
|
|
{tenant.logoUrl && (
|
|
<Button variant="ghost" size="sm" className="text-destructive" onClick={handleLogoDelete}>
|
|
<X className="w-3.5 h-3.5 mr-1" /> Entfernen
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<p className="text-[11px] text-muted-foreground">PNG, JPEG, SVG oder WebP, max. 2 MB</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div><Label className="text-xs">Name</Label><Input value={name} onChange={e => setName(e.target.value)} /></div>
|
|
<div><Label className="text-xs">Slug</Label><Input value={tenant.slug} disabled className="font-mono bg-muted" /></div>
|
|
</div>
|
|
<div><Label className="text-xs">Beschreibung</Label><Input value={description} onChange={e => setDescription(e.target.value)} /></div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div><Label className="text-xs">Kontakt E-Mail</Label><Input type="email" value={contactEmail} onChange={e => setContactEmail(e.target.value)} placeholder="kontakt@firma.ch" /></div>
|
|
<div><Label className="text-xs">Kontakt Telefon</Label><Input value={contactPhone} onChange={e => setContactPhone(e.target.value)} placeholder="+41 ..." /></div>
|
|
</div>
|
|
<div><Label className="text-xs">Adresse</Label><Input value={address} onChange={e => setAddress(e.target.value)} placeholder="Strasse, PLZ Ort" /></div>
|
|
<div><Label className="text-xs">Interne Notizen</Label><Input value={notes} onChange={e => setNotes(e.target.value)} placeholder="Interne Anmerkungen..." /></div>
|
|
<div className="flex gap-2 pt-2">
|
|
<Button onClick={handleSave} disabled={!name.trim()}>Speichern</Button>
|
|
<Button variant={status === 'SUSPENDED' ? 'default' : 'destructive'} onClick={handleSuspend}>
|
|
{status === 'SUSPENDED' ? <><CheckCircle className="w-4 h-4 mr-1" /> Aktivieren</> : <><Ban className="w-4 h-4 mr-1" /> Sperren</>}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Delete section */}
|
|
<div className="border-t pt-4 mt-4">
|
|
{!confirmDelete ? (
|
|
<Button variant="ghost" className="text-destructive hover:text-destructive text-xs" onClick={() => setConfirmDelete(true)}>
|
|
<Trash2 className="w-3.5 h-3.5 mr-1" /> Mandant löschen...
|
|
</Button>
|
|
) : (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 space-y-3">
|
|
<p className="text-sm font-medium text-red-800">Mandant unwiderruflich löschen?</p>
|
|
<p className="text-xs text-red-600">Alle Benutzer, Projekte und Daten dieses Mandanten werden gelöscht. Geben Sie <strong>{tenant.slug}</strong> ein zur Bestätigung:</p>
|
|
<Input
|
|
value={deleteSlug}
|
|
onChange={e => setDeleteSlug(e.target.value)}
|
|
placeholder={tenant.slug}
|
|
className="font-mono text-sm"
|
|
/>
|
|
<div className="flex gap-2">
|
|
<Button variant="destructive" size="sm" onClick={handleDeleteTenant} disabled={deleteSlug !== tenant.slug}>
|
|
<Trash2 className="w-3.5 h-3.5 mr-1" /> Endgültig löschen
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => { setConfirmDelete(false); setDeleteSlug('') }}>Abbrechen</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* === MEMBERS TAB === */}
|
|
<TabsContent value="members" className="space-y-4 mt-3">
|
|
<div className="border rounded-lg p-4 space-y-3 bg-muted/20">
|
|
<h4 className="text-sm font-semibold flex items-center gap-1.5"><UserPlus className="w-4 h-4" /> Neuen Benutzer erstellen</h4>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div><Label className="text-xs">Name</Label><Input value={newUserName} onChange={e => setNewUserName(e.target.value)} placeholder="Vor- und Nachname" /></div>
|
|
<div><Label className="text-xs">E-Mail</Label><Input type="email" value={newUserEmail} onChange={e => setNewUserEmail(e.target.value)} placeholder="name@feuerwehr.ch" /></div>
|
|
</div>
|
|
<div className="flex gap-3 items-end">
|
|
<div className="w-44">
|
|
<Label className="text-xs">Rolle</Label>
|
|
<Select value={newUserRole} onValueChange={setNewUserRole}>
|
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="TENANT_ADMIN">Admin</SelectItem>
|
|
<SelectItem value="OPERATOR">Bediener</SelectItem>
|
|
<SelectItem value="VIEWER">Betrachter</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button onClick={handleAddMember} disabled={!newUserName.trim() || !newUserEmail.trim() || addingUser}>
|
|
{addingUser ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : <UserPlus className="w-4 h-4 mr-1.5" />}
|
|
Benutzer erstellen
|
|
</Button>
|
|
</div>
|
|
<p className="text-[11px] text-muted-foreground">Ein temporäres Passwort wird automatisch generiert. Falls SMTP konfiguriert ist, wird eine Willkommens-E-Mail gesendet.</p>
|
|
</div>
|
|
|
|
{/* Created user info */}
|
|
{createdUserInfo && (
|
|
<div className="border rounded-lg p-4 bg-green-50 border-green-200 space-y-2">
|
|
<h4 className="text-sm font-semibold text-green-800 flex items-center gap-1.5">
|
|
<CheckCircle className="w-4 h-4" /> Benutzer erstellt
|
|
</h4>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
<div><span className="text-green-700 font-medium">E-Mail:</span> {createdUserInfo.email}</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-green-700 font-medium">Passwort:</span>
|
|
<code className="bg-white px-2 py-0.5 rounded text-sm font-mono">{createdUserInfo.tempPassword}</code>
|
|
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => {
|
|
navigator.clipboard.writeText(createdUserInfo.tempPassword)
|
|
toast({ title: 'Passwort kopiert' })
|
|
}}>
|
|
<Copy className="w-3.5 h-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-xs">
|
|
{createdUserInfo.emailSent ? (
|
|
<><Mail className="w-3.5 h-3.5 text-green-600" /><span className="text-green-700">Willkommens-E-Mail wurde gesendet</span></>
|
|
) : (
|
|
<><MailX className="w-3.5 h-3.5 text-orange-500" /><span className="text-orange-600">Keine E-Mail gesendet (SMTP nicht konfiguriert). Bitte Passwort manuell weitergeben.</span></>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Members list */}
|
|
<div className="border rounded-lg divide-y">
|
|
{tenant.memberships.length === 0 ? (
|
|
<div className="text-center text-muted-foreground py-6 text-sm">Keine Benutzer zugeordnet</div>
|
|
) : tenant.memberships.map(m => (
|
|
<div key={m.id} className="flex items-center justify-between px-3 py-2 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-bold text-white ${
|
|
m.role === 'TENANT_ADMIN' ? 'bg-orange-500' : m.role === 'OPERATOR' ? 'bg-blue-500' : 'bg-gray-400'
|
|
}`}>{m.user.name.charAt(0).toUpperCase()}</div>
|
|
<div>
|
|
<p className="font-medium">{m.user.name}</p>
|
|
<p className="text-xs text-muted-foreground">{m.user.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-[10px] text-muted-foreground" title="Zuletzt online">
|
|
{m.user.lastLoginAt
|
|
? new Date(m.user.lastLoginAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: '2-digit' }) + ' ' + new Date(m.user.lastLoginAt).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })
|
|
: 'Nie eingeloggt'}
|
|
</span>
|
|
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
|
m.role === 'TENANT_ADMIN' ? 'bg-orange-100 text-orange-700' :
|
|
m.role === 'OPERATOR' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
|
}`}>{m.role === 'TENANT_ADMIN' ? 'Admin' : m.role === 'OPERATOR' ? 'Bediener' : 'Betrachter'}</span>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => handleRemoveMember(m.id)}>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{tenant.memberships.length} Benutzer
|
|
</p>
|
|
</TabsContent>
|
|
|
|
{/* Subscription tab removed — SERVER_ADMIN only suspends/activates orgs */}
|
|
</Tabs>
|
|
) : (
|
|
<div className="text-center text-muted-foreground py-8">Mandant nicht gefunden</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|