Files
breakpilot-compliance/admin-compliance/app/sdk/multi-tenant/_components/TenantCard.tsx
Sharang Parnerkar dca0c96f2a refactor(admin): split multi-tenant page.tsx into colocated components
Extract types, constants, helpers, and UI pieces (LoadingSkeleton,
EmptyState, StatCard, ComplianceRing, Modal, TenantCard,
CreateTenantModal, EditTenantModal, TenantDetailModal) into
_components/ and _types.ts to bring page.tsx from 1663 LOC to
432 LOC (under the 500 hard cap). Behavior preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:47:59 +02:00

152 lines
6.1 KiB
TypeScript

'use client'
import { useState } from 'react'
import {
AlertTriangle,
ChevronDown,
ChevronUp,
Eye,
Globe,
GraduationCap,
Pencil,
Shield,
Truck,
Users,
} from 'lucide-react'
import type { TenantOverview } from '../_types'
import { formatDate, formatDateTime, getRiskBadgeClasses, getStatusBadge } from './helpers'
import { ComplianceRing } from './ComplianceRing'
export function TenantCard({
tenant,
onEdit,
onViewDetails,
onSwitchTenant,
}: {
tenant: TenantOverview
onEdit: (t: TenantOverview) => void
onViewDetails: (t: TenantOverview) => void
onSwitchTenant: (t: TenantOverview) => void
}) {
const [expanded, setExpanded] = useState(false)
const statusInfo = getStatusBadge(tenant.status)
return (
<div className="bg-white rounded-xl border border-slate-200 hover:border-indigo-300 hover:shadow-md transition-all overflow-hidden">
{/* Card Header */}
<div className="p-5">
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-base font-semibold text-slate-900 truncate">{tenant.name}</h3>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full ${statusInfo.bg} ${statusInfo.text}`}>
{statusInfo.icon}
{statusInfo.label}
</span>
</div>
<p className="text-xs text-slate-400 font-mono">{tenant.slug}</p>
</div>
<ComplianceRing score={tenant.compliance_score} size={56} />
</div>
{/* Risk Level */}
<div className="flex items-center gap-2 mb-4">
<span className={`inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full ${getRiskBadgeClasses(tenant.risk_level)}`}>
<Shield className="w-3 h-3" />
{tenant.risk_level}
</span>
<span className="text-xs text-slate-400">
{tenant.namespace_count} Namespace{tenant.namespace_count !== 1 ? 's' : ''}
</span>
<span className="text-xs text-slate-400 ml-auto">
{formatDate(tenant.created_at)}
</span>
</div>
{/* Quick Metrics */}
<div className="grid grid-cols-2 gap-2 mb-4">
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
<AlertTriangle className="w-3.5 h-3.5 text-orange-500" />
<span>{tenant.open_incidents} Vorfaelle</span>
</div>
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
<Shield className="w-3.5 h-3.5 text-indigo-500" />
<span>{tenant.open_reports} Meldungen</span>
</div>
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
<Users className="w-3.5 h-3.5 text-blue-500" />
<span>{tenant.pending_dsrs} DSRs</span>
</div>
<div className="flex items-center gap-1.5 text-xs text-slate-600 bg-slate-50 rounded-lg px-2.5 py-1.5">
<GraduationCap className="w-3.5 h-3.5 text-green-500" />
<span>{tenant.training_completion_rate}% Training</span>
</div>
</div>
{/* Vendor Risk Info */}
{tenant.vendor_risk_high > 0 && (
<div className="flex items-center gap-1.5 text-xs text-orange-600 bg-orange-50 rounded-lg px-2.5 py-1.5 mb-4">
<Truck className="w-3.5 h-3.5" />
<span>{tenant.vendor_risk_high} Dienstleister mit hohem Risiko</span>
</div>
)}
{/* Actions */}
<div className="flex items-center gap-2">
<button
onClick={() => onViewDetails(tenant)}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-indigo-600 bg-indigo-50 hover:bg-indigo-100 rounded-lg transition-colors"
>
<Eye className="w-3.5 h-3.5" />
Details
</button>
<button
onClick={() => onEdit(tenant)}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-slate-600 bg-slate-50 hover:bg-slate-100 rounded-lg transition-colors"
>
<Pencil className="w-3.5 h-3.5" />
Bearbeiten
</button>
<button
onClick={() => onSwitchTenant(tenant)}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors ml-auto"
>
<Globe className="w-3.5 h-3.5" />
Wechseln
</button>
<button
onClick={() => setExpanded(!expanded)}
className="p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
>
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
</div>
</div>
{/* Expanded Section */}
{expanded && (
<div className="border-t border-slate-100 bg-slate-50/50 px-5 py-4 space-y-3">
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<span className="text-slate-400">Max. Benutzer</span>
<p className="font-semibold text-slate-700">{tenant.max_users.toLocaleString('de-DE')}</p>
</div>
<div>
<span className="text-slate-400">LLM Kontingent / Monat</span>
<p className="font-semibold text-slate-700">{tenant.llm_quota_monthly.toLocaleString('de-DE')}</p>
</div>
<div>
<span className="text-slate-400">Compliance Score</span>
<p className="font-semibold text-slate-700">{tenant.compliance_score} / 100</p>
</div>
<div>
<span className="text-slate-400">Aktualisiert</span>
<p className="font-semibold text-slate-700">{formatDateTime(tenant.updated_at)}</p>
</div>
</div>
</div>
)}
</div>
)
}