'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(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) => { 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 ( {tenant?.name || 'Mandant'} {statusInfo && ( s.value === tenant?.subscriptionStatus)?.color || ''}`}> {statusInfo.label} )} {loading ? (
) : tenant ? ( Stammdaten Benutzer ({tenant.memberships.length}) {/* === INFO TAB === */} {/* Logo */}
{tenant.logoUrl ? ( Logo ) : ( )}
{tenant.logoUrl && ( )}

PNG, JPEG, SVG oder WebP, max. 2 MB

setName(e.target.value)} />
setDescription(e.target.value)} />
setContactEmail(e.target.value)} placeholder="kontakt@firma.ch" />
setContactPhone(e.target.value)} placeholder="+41 ..." />
setAddress(e.target.value)} placeholder="Strasse, PLZ Ort" />
setNotes(e.target.value)} placeholder="Interne Anmerkungen..." />
{/* Delete section */}
{!confirmDelete ? ( ) : (

Mandant unwiderruflich löschen?

Alle Benutzer, Projekte und Daten dieses Mandanten werden gelöscht. Geben Sie {tenant.slug} ein zur Bestätigung:

setDeleteSlug(e.target.value)} placeholder={tenant.slug} className="font-mono text-sm" />
)}
{/* === MEMBERS TAB === */}

Neuen Benutzer erstellen

setNewUserName(e.target.value)} placeholder="Vor- und Nachname" />
setNewUserEmail(e.target.value)} placeholder="name@feuerwehr.ch" />

Ein temporäres Passwort wird automatisch generiert. Falls SMTP konfiguriert ist, wird eine Willkommens-E-Mail gesendet.

{/* Created user info */} {createdUserInfo && (

Benutzer erstellt

E-Mail: {createdUserInfo.email}
Passwort: {createdUserInfo.tempPassword}
{createdUserInfo.emailSent ? ( <>Willkommens-E-Mail wurde gesendet ) : ( <>Keine E-Mail gesendet (SMTP nicht konfiguriert). Bitte Passwort manuell weitergeben. )}
)} {/* Members list */}
{tenant.memberships.length === 0 ? (
Keine Benutzer zugeordnet
) : tenant.memberships.map(m => (
{m.user.name.charAt(0).toUpperCase()}

{m.user.name}

{m.user.email}

{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'} {m.role === 'TENANT_ADMIN' ? 'Admin' : m.role === 'OPERATOR' ? 'Bediener' : 'Betrachter'}
))}

{tenant.memberships.length} Benutzer

{/* Subscription tab removed — SERVER_ADMIN only suspends/activates orgs */}
) : (
Mandant nicht gefunden
)}
) }