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>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
'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">📋</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)`}
|
||||
>
|
||||
⚠
|
||||
</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 →
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user