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>
231 lines
8.8 KiB
TypeScript
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">📋</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>
|
|
)
|
|
}
|