'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { ScrollArea } from '@/components/ui/scroll-area' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useToast } from '@/components/ui/use-toast' import { useAuth } from '@/components/providers/auth-provider' import { Plus, Upload, Pencil, Trash2, FolderPlus, ArrowLeft, Eye, EyeOff, Users, Settings, Shield, UserPlus, Image, Layers, Loader2, MapPin, CheckCircle, Ban, Clock, KeyRound, Copy, Heart, Map, ShieldCheck, ClipboardList, X, BookOpen, AlertTriangle, LayoutGrid, Building2, } from 'lucide-react' import Link from 'next/link' import { TenantDetailDialog } from '@/components/admin/tenant-detail-dialog' import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog' import { SettingsTab } from '@/components/admin/settings-tab' import { SomaTab } from '@/components/admin/soma-tab' import { SuggestionsTab } from '@/components/admin/suggestions-tab' import { DictionaryTab } from '@/components/admin/dictionary-tab' import { SymbolManager } from '@/components/admin/symbol-manager' import { OrgTab } from '@/components/admin/org-tab' // --- Types --- interface IconCategory { id: string name: string description: string | null sortOrder: number _count?: { icons: number } } interface IconAsset { id: string name: string fileKey: string mimeType: string isSystem: boolean isActive: boolean iconType: string tags: string[] category: IconCategory } interface UserRecord { id: string email: string name: string role: 'SERVER_ADMIN' | 'TENANT_ADMIN' | 'OPERATOR' | 'VIEWER' emailVerified?: boolean createdAt: string updatedAt: string memberships?: { tenant: { id: string; name: string; slug: string } }[] _count?: { projects: number } } interface TenantRecord { id: string name: string slug: string description: string | null isActive: boolean createdAt: string _count?: { memberships: number; projects: number } } const ICON_TYPES = [ { value: 'STANDARD', label: 'Standard' }, { value: 'RETTUNG', label: 'Rettung' }, { value: 'GEFAHRSTOFF', label: 'Gefahrstoff' }, { value: 'FEUER', label: 'Feuer' }, { value: 'WASSER', label: 'Wasser' }, { value: 'FAHRZEUG', label: 'Fahrzeug' }, ] const ROLES = [ { value: 'SERVER_ADMIN', label: 'Server Admin', desc: 'Systemweiter Vollzugriff, Mandanten verwalten' }, { value: 'TENANT_ADMIN', label: 'Admin', desc: 'Mandant verwalten, Benutzer anlegen' }, { value: 'OPERATOR', label: 'Bediener', desc: 'Einsätze erstellen und bearbeiten' }, { value: 'VIEWER', label: 'Betrachter', desc: 'Nur Ansicht, kein Bearbeiten' }, ] export default function AdminPage() { const { toast } = useToast() const { user, tenant, loading: authLoading, login: authLogin } = useAuth() const router = useRouter() const [categories, setCategories] = useState([]) const [icons, setIcons] = useState([]) const [users, setUsers] = useState([]) const [tenants, setTenants] = useState([]) const [selectedCategory, setSelectedCategory] = useState('all') const [isLoading, setIsLoading] = useState(true) const [activeTab, setActiveTab] = useState(user?.role === 'SERVER_ADMIN' ? 'tenants' : 'org') // Category Dialog const [isCategoryDialogOpen, setIsCategoryDialogOpen] = useState(false) const [editingCategory, setEditingCategory] = useState(null) const [categoryName, setCategoryName] = useState('') const [categoryDescription, setCategoryDescription] = useState('') // Icon Upload Dialog const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false) const [uploadFiles, setUploadFiles] = useState(null) const [uploadCategory, setUploadCategory] = useState('') const [uploadIconType, setUploadIconType] = useState('STANDARD') const [uploadIconName, setUploadIconName] = useState('') const [isUploading, setIsUploading] = useState(false) // Icon Edit Dialog const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [editingIcon, setEditingIcon] = useState(null) const [editIconName, setEditIconName] = useState('') const [editIconCategory, setEditIconCategory] = useState('') const [editIconType, setEditIconType] = useState('STANDARD') const [editIconTags, setEditIconTags] = useState('') // User Dialog const [isUserDialogOpen, setIsUserDialogOpen] = useState(false) const [editingUser, setEditingUser] = useState(null) const [userName, setUserName] = useState('') const [userEmail, setUserEmail] = useState('') const [userPassword, setUserPassword] = useState('') const [userPasswordConfirm, setUserPasswordConfirm] = useState('') const [userRole, setUserRole] = useState('OPERATOR') // Tenant Dialog const [isTenantDialogOpen, setIsTenantDialogOpen] = useState(false) const [tenantName, setTenantName] = useState('') const [tenantSlug, setTenantSlug] = useState('') const [tenantDescription, setTenantDescription] = useState('') // Tenant Detail Dialog const [selectedTenantId, setSelectedTenantId] = useState(null) const [isTenantDetailOpen, setIsTenantDetailOpen] = useState(false) // Admin Projects (SERVER_ADMIN) const [adminProjects, setAdminProjects] = useState([]) const [adminProjectsLoading, setAdminProjectsLoading] = useState(false) const [adminProjectTenantFilter, setAdminProjectTenantFilter] = useState('all') // Hose Settings (Tenant Admin) const [isHoseSettingsOpen, setIsHoseSettingsOpen] = useState(false) // Redirect to login if not authenticated, or to app if not admin useEffect(() => { if (authLoading) return if (!user) { router.push('/login') } else if (user.role !== 'SERVER_ADMIN' && user.role !== 'TENANT_ADMIN') { router.push('/app') } }, [authLoading, user, router]) useEffect(() => { if (user?.role) fetchData() }, [user?.role]) // Fetch admin projects (SERVER_ADMIN) const fetchAdminProjects = async (tenantFilter?: string) => { setAdminProjectsLoading(true) try { const url = tenantFilter && tenantFilter !== 'all' ? `/api/admin/projects?tenantId=${tenantFilter}` : '/api/admin/projects' const res = await fetch(url) if (res.ok) { const data = await res.json() setAdminProjects(data.projects || []) } } catch {} setAdminProjectsLoading(false) } useEffect(() => { if (user?.role === 'SERVER_ADMIN') fetchAdminProjects() }, [user?.role]) const fetchData = async () => { setIsLoading(true) try { const isServerAdmin = user?.role === 'SERVER_ADMIN' // Common fetches for all admins const fetches: Promise[] = [ fetch('/api/admin/categories'), fetch('/api/admin/users'), ] // SERVER_ADMIN-only fetches if (isServerAdmin) { fetches.push(fetch('/api/admin/icons')) fetches.push(fetch('/api/admin/tenants')) } const results = await Promise.all(fetches) const [catRes, userRes] = results if (catRes.ok) setCategories((await catRes.json()).categories || []) if (userRes.ok) setUsers((await userRes.json()).users || []) if (isServerAdmin) { const iconRes = results[2] const tenantRes = results[3] if (iconRes.ok) setIcons((await iconRes.json()).icons || []) if (tenantRes.ok) setTenants((await tenantRes.json()).tenants || []) } } catch (error) { console.error('Error fetching data:', error) } finally { setIsLoading(false) } } // ===== CATEGORY FUNCTIONS ===== const handleSaveCategory = async () => { try { const url = editingCategory ? `/api/admin/categories/${editingCategory.id}` : '/api/admin/categories' const method = editingCategory ? 'PATCH' : 'POST' const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: categoryName, description: categoryDescription || null }), }) if (res.ok) { toast({ title: editingCategory ? 'Kategorie aktualisiert' : 'Kategorie erstellt' }) setIsCategoryDialogOpen(false) setEditingCategory(null) setCategoryName('') setCategoryDescription('') fetchData() } 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 handleDeleteCategory = async (id: string) => { if (!confirm('Kategorie wirklich löschen?')) return try { const res = await fetch(`/api/admin/categories/${id}`, { method: 'DELETE' }) if (res.ok) { toast({ title: 'Kategorie gelöscht' }); fetchData() } } catch { toast({ title: 'Fehler', variant: 'destructive' }) } } // ===== ICON FUNCTIONS ===== const handleUploadIcons = async () => { if (!uploadFiles || !uploadCategory) return setIsUploading(true) try { for (const file of Array.from(uploadFiles)) { const formData = new FormData() formData.append('file', file) formData.append('categoryId', uploadCategory) formData.append('iconType', uploadIconType) formData.append('name', uploadIconName.trim() || file.name.replace(/\.(png|svg|jpg|jpeg|webp)$/i, '')) const res = await fetch('/api/admin/icons/upload', { method: 'POST', body: formData }) if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Upload fehlgeschlagen') } } toast({ title: `${uploadFiles.length} Icon(s) hochgeladen` }) setIsUploadDialogOpen(false) setUploadFiles(null) setUploadCategory('') setUploadIconName('') fetchData() } catch (error) { toast({ title: 'Upload-Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' }) } finally { setIsUploading(false) } } const handleEditIcon = (icon: IconAsset) => { setEditingIcon(icon) setEditIconName(icon.name) setEditIconCategory(icon.category.id) setEditIconType(icon.iconType) setEditIconTags(icon.tags?.join(', ') || '') setIsEditDialogOpen(true) } const handleSaveIcon = async () => { if (!editingIcon) return try { const res = await fetch(`/api/admin/icons/${editingIcon.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: editIconName, categoryId: editIconCategory, iconType: editIconType, tags: editIconTags.split(',').map(t => t.trim()).filter(Boolean), }), }) if (res.ok) { toast({ title: 'Icon aktualisiert' }); setIsEditDialogOpen(false); setEditingIcon(null); fetchData() } 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 handleToggleIconActive = async (icon: IconAsset) => { try { if (user?.role === 'SERVER_ADMIN') { // SERVER_ADMIN toggles global isActive flag directly const res = await fetch(`/api/admin/icons/${icon.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isActive: !icon.isActive }), }) if (res.ok) { toast({ title: icon.isActive ? 'Symbol deaktiviert' : 'Symbol aktiviert' }) fetchData() } else { const err = await res.json() toast({ title: 'Fehler', description: err.error, variant: 'destructive' }) } } else { // TENANT_ADMIN uses per-tenant visibility toggle const res = await fetch(`/api/icons/${icon.id}/toggle-visibility`, { method: 'POST' }) if (res.ok) { const data = await res.json() toast({ title: data.message || (data.isHidden ? 'Symbol ausgeblendet' : 'Symbol eingeblendet') }) fetchData() } else { const err = await res.json() toast({ title: 'Fehler', description: err.error, variant: 'destructive' }) } } } catch { toast({ title: 'Fehler', variant: 'destructive' }) } } const handleDeleteIcon = async (id: string) => { if (!confirm('Icon wirklich löschen?')) return try { const res = await fetch(`/api/admin/icons/${id}`, { method: 'DELETE' }) if (res.ok) { toast({ title: 'Icon gelöscht' }); fetchData() } } catch { toast({ title: 'Fehler', variant: 'destructive' }) } } // ===== USER FUNCTIONS ===== const handleSaveUser = async () => { try { // Validate password confirmation if (userPassword && userPassword !== userPasswordConfirm) { toast({ title: 'Fehler', description: 'Passwörter stimmen nicht überein.', variant: 'destructive' }) return } const url = editingUser ? `/api/admin/users/${editingUser.id}` : '/api/admin/users' const method = editingUser ? 'PATCH' : 'POST' const body: any = { name: userName, email: userEmail, role: userRole } if (userPassword) body.password = userPassword const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (res.ok) { toast({ title: editingUser ? 'Benutzer aktualisiert' : 'Benutzer erstellt' }) setIsUserDialogOpen(false) setEditingUser(null) setUserName('') setUserEmail('') setUserPassword('') setUserPasswordConfirm('') setUserRole('OPERATOR') fetchData() } 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 handleDeleteUser = async (id: string) => { if (!confirm('Benutzer wirklich löschen?')) return try { const res = await fetch(`/api/admin/users/${id}`, { method: 'DELETE' }) if (res.ok) { toast({ title: 'Benutzer gelöscht' }); fetchData() } } catch { toast({ title: 'Fehler', variant: 'destructive' }) } } const handleToggleUserVerified = async (targetUser: UserRecord) => { try { const newVal = !targetUser.emailVerified const res = await fetch(`/api/admin/users/${targetUser.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailVerified: newVal }), }) if (res.ok) { toast({ title: newVal ? 'Benutzer freigeschaltet' : 'Benutzer gesperrt' }) fetchData() } else { const err = await res.json() toast({ title: 'Fehler', description: err.error, variant: 'destructive' }) } } catch { toast({ title: 'Fehler', variant: 'destructive' }) } } const handleResetUserPassword = async (targetUser: UserRecord) => { try { const res = await fetch(`/api/admin/users/${targetUser.id}/reset-password`, { method: 'POST' }) const data = await res.json() if (res.ok && data.success) { if (data.emailSent) { toast({ title: 'Reset-Link gesendet', description: data.message }) } else if (data.resetUrl) { // No SMTP → show/copy link await navigator.clipboard.writeText(data.resetUrl).catch(() => {}) toast({ title: 'Reset-Link generiert', description: 'Link wurde in die Zwischenablage kopiert. Kein SMTP konfiguriert.', }) } } else { toast({ title: 'Fehler', description: data.error, variant: 'destructive' }) } } catch { toast({ title: 'Fehler', variant: 'destructive' }) } } // ===== TENANT FUNCTIONS ===== const handleSaveTenant = async () => { try { const res = await fetch('/api/admin/tenants', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: tenantName, slug: tenantSlug, description: tenantDescription || undefined }), }) if (res.ok) { toast({ title: 'Mandant erstellt' }) setIsTenantDialogOpen(false) setTenantName('') setTenantSlug('') setTenantDescription('') fetchData() } 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 filteredIcons = selectedCategory === 'all' ? icons : icons.filter(i => i.category.id === selectedCategory) if (isLoading || authLoading || !user || (user.role !== 'SERVER_ADMIN' && user.role !== 'TENANT_ADMIN')) { return (
) } return (
{/* Header */}

Administration

{user && (
{user.name} {user.role}
)}
{user?.role === 'SERVER_ADMIN' ? ( Mandanten Einsätze Symbole Kategorien Wörterbuch Server Admins System ) : user?.role === 'TENANT_ADMIN' ? ( Organisation Benutzer Symbole SOMA Wörterliste Schläuche Spenden ) : null} {/* ===== ORGANISATION TAB (TENANT_ADMIN) ===== */} {user?.role === 'TENANT_ADMIN' && ( )} {/* ===== ICONS TAB ===== */} {user?.role === 'TENANT_ADMIN' ? ( ) : ( /* --- SERVER_ADMIN: existing icon management --- */ <>
{filteredIcons.length} Symbol(e)
{filteredIcons.length === 0 ? (

Keine Symbole vorhanden

Laden Sie eigene Symbole hoch (PNG, SVG, JPEG)

) : (
{filteredIcons.map(icon => (
{icon.name}

{icon.name}

{icon.category.name}

{icon.isSystem &&

System

}
))}
)} )}
{/* ===== CATEGORIES TAB ===== */}

{categories.length} Kategorie(n)

{categories.map(cat => (

{cat.name}

{cat.description &&

{cat.description}

}

{cat._count?.icons || 0} Symbol(e) · Reihenfolge: {cat.sortOrder}

))}
{/* ===== USERS TAB ===== */}

{user?.role === 'SERVER_ADMIN' ? `${users.filter(u => u.role === 'SERVER_ADMIN').length} Server-Admin(s)` : `${users.length} Benutzer` }

{(user?.role === 'SERVER_ADMIN' ? users.filter(u => u.role === 'SERVER_ADMIN') : users).map(u => ( ))}
Name E-Mail Rolle Projekte Aktionen
{u.name.charAt(0).toUpperCase()}
{u.name}
{u.email} {u.emailVerified === false && ( unverifiziert )}
{ROLES.find(r => r.value === u.role)?.label || u.role} {u._count?.projects || 0}
{u.emailVerified === false && ( )}
{/* ===== TENANTS TAB (SERVER_ADMIN only) ===== */} {user?.role === 'SERVER_ADMIN' && (

{tenants.length} Mandant(en)

{user?.role === 'SERVER_ADMIN' && ( )}
{tenants.length === 0 ? (

Keine Mandanten

Erstellen Sie einen Mandanten (Organisation/Feuerwehr)

) : (
{tenants.map(t => (
{ setSelectedTenantId(t.id); setIsTenantDetailOpen(true) }}>

{t.name}

{t.slug}

{t.description &&

{t.description}

}
{t._count?.memberships || 0} Benutzer {t._count?.projects || 0} Projekte
{t.isActive ? 'Aktiv' : 'Inaktiv'}
))}
)}
)} {/* ===== PROJECTS TAB (SERVER_ADMIN — Einsätze verwalten) ===== */} {user?.role === 'SERVER_ADMIN' && (

{adminProjects.length} Einsatz/Einsätze

Feuerwehr:
{adminProjectsLoading ? (
) : adminProjects.length === 0 ? (

Keine Einsätze gefunden.

) : (
{adminProjects.map((p: any) => ( ))}
Einsatz-Nr Titel Ort Erstellt von Feuerwehr Elemente Geändert Aktion
{p.einsatzNr || '—'} {p.title} {p.location || '—'} {p.owner?.name || p.owner?.email || '—'} {p.tenant?.name || '—'} {p._count?.features || 0} {new Date(p.updatedAt).toLocaleString('de-CH')}
)}
)} {/* ===== HOSE TYPES TAB (Schlauchtypen) ===== */}

Schlauchtypen verwalten

Konfiguriere die Schlauchtypen für die Druckberechnung im Messwerkzeug. Der Standard-Schlauch wird automatisch für neue Berechnungen verwendet.

{/* ===== SUGGESTIONS TAB (Word Library) ===== */} {/* ===== DICTIONARY TAB (SERVER_ADMIN — Global Word Library) ===== */} {user?.role === 'SERVER_ADMIN' && ( )} {/* ===== SETTINGS TAB (SERVER_ADMIN only) ===== */} {user?.role === 'SERVER_ADMIN' && ( )} {/* ===== SPENDEN TAB (TENANT_ADMIN) ===== */} {user?.role === 'TENANT_ADMIN' && tenant && (

Lageplan unterstützen

Lageplan ist ein kostenloses Projekt — entwickelt von einem aktiven Feuerwehrmann in seiner Freizeit. Ohne Firma, ohne Investoren. Deine Spende hilft, den Betrieb und die Weiterentwicklung zu finanzieren.

Wohin fliesst deine Spende?

  • Server-Hosting in der Schweiz (monatliche Kosten)
  • Entwicklung neuer Features und Bugfixes
  • Domain, SSL-Zertifikate und Infrastruktur

Dein Mandant

Organisation

{tenant.name}

Benutzer

Unbegrenzt

Projekte

Unbegrenzt

)} {/* ===== SOMA TAB (TENANT_ADMIN) ===== */} {user?.role === 'TENANT_ADMIN' && ( )} {/* Upgrades tab removed — plan management simplified */}
{/* ===== DIALOGS ===== */} {/* Category Dialog */} {editingCategory ? 'Kategorie bearbeiten' : 'Neue Kategorie'}
setCategoryName(e.target.value)} placeholder="z.B. Fahrzeuge" />
setCategoryDescription(e.target.value)} placeholder="Kurze Beschreibung" />
{/* Upload Dialog */} Icons hochladen
setUploadFiles(e.target.files)} />
setUploadIconName(e.target.value)} placeholder="z.B. Feuerwehrauto TLF" />
{/* Edit Icon Dialog */} Icon bearbeiten
{editingIcon && (
{editingIcon.name}
)}
setEditIconName(e.target.value)} />
setEditIconTags(e.target.value)} placeholder="z.B. feuerwehr, fahrzeug" />
{/* User Dialog */} {editingUser ? 'Benutzer bearbeiten' : 'Neuer Benutzer'}
setUserName(e.target.value)} placeholder="Vor- und Nachname" />
setUserEmail(e.target.value)} placeholder="name@example.com" />
setUserPassword(e.target.value)} placeholder={editingUser ? '••••••••' : 'Min. 6 Zeichen'} />
setUserPasswordConfirm(e.target.value)} placeholder="Passwort wiederholen" /> {userPassword && userPasswordConfirm && userPassword !== userPasswordConfirm && (

Passwörter stimmen nicht überein

)}
{/* Tenant Dialog */} Neuer Mandant
{ setTenantName(e.target.value); setTenantSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }} placeholder="z.B. Feuerwehr Wohlen" />
setTenantSlug(e.target.value)} placeholder="z.B. feuerwehr-wohlen" className="font-mono" />
setTenantDescription(e.target.value)} placeholder="Kurze Beschreibung" />
{/* Tenant Detail Dialog */}
) } // ─── Upgrade Requests Tab (SERVER_ADMIN) ────────────────────── function UpgradeRequestsTab({ toast }: { toast: any }) { const [requests, setRequests] = useState([]) const [loading, setLoading] = useState(true) const [processingId, setProcessingId] = useState(null) const [adminNote, setAdminNote] = useState('') const [showNoteFor, setShowNoteFor] = useState(null) useEffect(() => { fetchRequests() }, []) const fetchRequests = async () => { try { const res = await fetch('/api/upgrade-requests') if (res.ok) { const data = await res.json() setRequests(data.requests || []) } } catch (e) { console.error('Failed to fetch upgrade requests:', e) } finally { setLoading(false) } } const handleProcess = async (id: string, action: 'approve' | 'reject') => { setProcessingId(id) try { const res = await fetch(`/api/upgrade-requests/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, adminNote: adminNote.trim() || undefined }), }) const data = await res.json() if (!res.ok) throw new Error(data.error || 'Fehler') toast({ title: action === 'approve' ? 'Upgrade bestätigt' : 'Anfrage abgelehnt', description: action === 'approve' ? `Plan wurde aktiviert. Der Mandant wurde per E-Mail informiert.` : `Die Anfrage wurde abgelehnt. Der Mandant wurde informiert.`, }) setAdminNote('') setShowNoteFor(null) fetchRequests() } catch (e: any) { toast({ title: 'Fehler', description: e.message, variant: 'destructive' }) } finally { setProcessingId(null) } } const planLabels: Record = { FREE: 'Free', PRO: 'Pro' } const statusLabels: Record = { PENDING: { label: 'Offen', color: 'bg-yellow-100 text-yellow-800' }, APPROVED: { label: 'Bestätigt', color: 'bg-green-100 text-green-800' }, REJECTED: { label: 'Abgelehnt', color: 'bg-red-100 text-red-800' }, } const pendingRequests = requests.filter(r => r.status === 'PENDING') const processedRequests = requests.filter(r => r.status !== 'PENDING') if (loading) { return
} return (
{/* Pending requests */}

Offene Anfragen {pendingRequests.length > 0 && ( {pendingRequests.length} )}

{pendingRequests.length === 0 ? (
Keine offenen Upgrade-Anfragen
) : (
{pendingRequests.map((r: any) => (

{r.tenant?.name}

{r.requestedBy?.name} ({r.requestedBy?.email}) — {new Date(r.createdAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}

{planLabels[r.currentPlan]} → {planLabels[r.requestedPlan]}

{r.message && (

Nachricht:

{r.message}
)} {showNoteFor === r.id && (