Files
breakpilot-compliance/admin-compliance/app/sdk/loeschfristen/_components/UebersichtTab.tsx
Sharang Parnerkar 6c883fb12e refactor(admin): split loeschfristen + dsb-portal page.tsx into colocated components
Split two oversized page files into _components/ directories following
Next.js 15 conventions and the 500-LOC hard cap:

- loeschfristen/page.tsx (2322 LOC -> 412 LOC orchestrator + 6 components)
- dsb-portal/page.tsx (2068 LOC -> 135 LOC orchestrator + 9 components)

All component files stay under 500 lines. Build verified.

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

231 lines
8.8 KiB
TypeScript

'use client'
import React from 'react'
import {
LoeschfristPolicy, PolicyStatus, DeletionTriggerLevel,
STATUS_COLORS, STATUS_LABELS, TRIGGER_COLORS, TRIGGER_LABELS,
RETENTION_DRIVER_META, formatRetentionDuration, isPolicyOverdue,
getActiveLegalHolds, getEffectiveDeletionTrigger,
} from '@/lib/sdk/loeschfristen-types'
// ---------------------------------------------------------------------------
// Badge helpers
// ---------------------------------------------------------------------------
export function renderStatusBadge(status: PolicyStatus) {
const colors = STATUS_COLORS[status] ?? 'bg-gray-100 text-gray-800'
const label = STATUS_LABELS[status] ?? status
return (
<span className={`inline-block text-xs font-semibold px-2 py-0.5 rounded-full ${colors}`}>
{label}
</span>
)
}
export function renderTriggerBadge(trigger: DeletionTriggerLevel) {
const colors = TRIGGER_COLORS[trigger] ?? 'bg-gray-100 text-gray-800'
const label = TRIGGER_LABELS[trigger] ?? trigger
return (
<span className={`inline-block text-xs font-semibold px-2 py-0.5 rounded-full ${colors}`}>
{label}
</span>
)
}
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface UebersichtTabProps {
policies: LoeschfristPolicy[]
filteredPolicies: LoeschfristPolicy[]
stats: { total: number; active: number; draft: number; overdue: number; legalHolds: number }
searchQuery: string
setSearchQuery: (q: string) => void
filter: string
setFilter: (f: string) => void
driverFilter: string
setDriverFilter: (f: string) => void
setTab: (tab: 'uebersicht' | 'editor' | 'generator' | 'export') => void
setEditingId: (id: string | null) => void
createNewPolicy: () => void
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function UebersichtTab({
policies,
filteredPolicies,
stats,
searchQuery,
setSearchQuery,
filter,
setFilter,
driverFilter,
setDriverFilter,
setTab,
setEditingId,
createNewPolicy,
}: UebersichtTabProps) {
return (
<div className="space-y-6">
{/* Stats bar */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{[
{ label: 'Gesamt', value: stats.total, color: 'text-gray-900' },
{ label: 'Aktiv', value: stats.active, color: 'text-green-600' },
{ label: 'Entwurf', value: stats.draft, color: 'text-yellow-600' },
{ label: 'Pruefung faellig', value: stats.overdue, color: 'text-red-600' },
{ label: 'Legal Holds aktiv', value: stats.legalHolds, color: 'text-orange-600' },
].map((s) => (
<div key={s.label} className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className={`text-3xl font-bold ${s.color}`}>{s.value}</div>
<div className="text-sm text-gray-500 mt-1">{s.label}</div>
</div>
))}
</div>
{/* Search & filters */}
<div className="bg-white rounded-xl border border-gray-200 p-4 space-y-3">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Suche nach Name, ID oder Beschreibung..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
<div className="flex flex-wrap gap-2 items-center">
<span className="text-sm text-gray-500 font-medium">Status:</span>
{[
{ key: 'all', label: 'Alle' },
{ key: 'active', label: 'Aktiv' },
{ key: 'draft', label: 'Entwurf' },
{ key: 'review', label: 'Pruefung noetig' },
].map((f) => (
<button
key={f.key}
onClick={() => setFilter(f.key)}
className={`px-3 py-1 rounded-lg text-sm font-medium transition ${
filter === f.key
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f.label}
</button>
))}
<span className="text-sm text-gray-500 font-medium ml-4">Aufbewahrungstreiber:</span>
<select
value={driverFilter}
onChange={(e) => setDriverFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="all">Alle</option>
{Object.entries(RETENTION_DRIVER_META).map(([key, meta]) => (
<option key={key} value={key}>{meta.label}</option>
))}
</select>
</div>
</div>
{/* Policy cards or empty state */}
{filteredPolicies.length === 0 && policies.length === 0 ? (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="text-gray-400 text-5xl mb-4">&#128203;</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Noch keine Loeschfristen angelegt
</h3>
<p className="text-gray-500 mb-6">
Starten Sie den Generator, um auf Basis Ihres Unternehmensprofils
automatisch passende Loeschfristen zu erstellen, oder legen Sie
manuell eine neue Loeschfrist an.
</p>
<div className="flex justify-center gap-3">
<button
onClick={() => setTab('generator')}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition"
>
Generator starten
</button>
<button
onClick={createNewPolicy}
className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
>
Neue Loeschfrist
</button>
</div>
</div>
) : filteredPolicies.length === 0 ? (
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
<p className="text-gray-500">Keine Loeschfristen entsprechen den aktuellen Filtern.</p>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredPolicies.map((p) => {
const trigger = getEffectiveDeletionTrigger(p)
const activeHolds = getActiveLegalHolds(p)
const overdue = isPolicyOverdue(p)
return (
<div
key={p.policyId}
className="bg-white rounded-xl border border-gray-200 p-6 hover:shadow-md transition relative"
>
{activeHolds.length > 0 && (
<span
className="absolute top-3 right-3 text-orange-500"
title={`${activeHolds.length} aktive Legal Hold(s)`}
>
&#9888;
</span>
)}
<div className="text-xs text-gray-400 font-mono mb-1">{p.policyId}</div>
<h3 className="text-base font-semibold text-gray-900 mb-2 truncate">
{p.dataObjectName || 'Ohne Bezeichnung'}
</h3>
<div className="flex flex-wrap gap-1.5 mb-3">
{renderTriggerBadge(trigger)}
<span className="inline-block text-xs font-medium px-2 py-0.5 rounded-full bg-blue-100 text-blue-800">
{formatRetentionDuration(p)}
</span>
{renderStatusBadge(p.status)}
{overdue && (
<span className="inline-block text-xs font-semibold px-2 py-0.5 rounded-full bg-red-100 text-red-700">
Pruefung faellig
</span>
)}
</div>
{p.description && (
<p className="text-sm text-gray-500 mb-3 line-clamp-2">{p.description}</p>
)}
<button
onClick={() => {
setEditingId(p.policyId)
setTab('editor')
}}
className="text-sm text-purple-600 hover:text-purple-800 font-medium"
>
Bearbeiten &rarr;
</button>
</div>
)
})}
</div>
)}
{/* Floating action button */}
{policies.length > 0 && (
<div className="flex justify-end">
<button
onClick={createNewPolicy}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-5 py-2.5 font-medium transition shadow-sm"
>
+ Neue Loeschfrist
</button>
</div>
)}
</div>
)
}