Files
breakpilot-compliance/admin-compliance/app/sdk/dsb-portal/page.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

136 lines
5.3 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { DSBDashboard, AssignmentOverview, apiFetch } from './_components/types'
import {
useToast, ToastContainer, DashboardSkeleton, StatCard, ErrorState, EmptyState,
IconUsers, IconClock, IconTask, IconAlert, IconShield, IconRefresh,
} from './_components/ui-primitives'
import { MandantCard } from './_components/MandantCard'
import { DetailView } from './_components/DetailView'
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function DSBPortalPage() {
const [dashboard, setDashboard] = useState<DSBDashboard | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [selectedAssignment, setSelectedAssignment] = useState<AssignmentOverview | null>(null)
const { toasts, addToast } = useToast()
const fetchDashboard = useCallback(async () => {
setLoading(true)
setError('')
try {
const data = await apiFetch<DSBDashboard>('/api/sdk/v1/dsb/dashboard')
setDashboard(data)
if (selectedAssignment) {
const updated = data.assignments.find((a) => a.id === selectedAssignment.id)
if (updated) setSelectedAssignment(updated)
}
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden des Dashboards')
} finally {
setLoading(false)
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
fetchDashboard()
}, [fetchDashboard])
const handleBackToDashboard = () => {
setSelectedAssignment(null)
fetchDashboard()
}
return (
<div className="min-h-screen bg-gray-50">
<ToastContainer toasts={toasts} />
{/* Global Header */}
<div className="bg-gradient-to-r from-purple-700 via-violet-600 to-purple-800 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-white/20 backdrop-blur-sm flex items-center justify-center">
<IconShield className="w-7 h-7 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold">DSB-Portal</h1>
<p className="text-purple-200 text-sm">Datenschutzbeauftragter Mandanten-Verwaltung</p>
</div>
</div>
<button onClick={fetchDashboard}
className="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors" title="Aktualisieren">
<IconRefresh className="w-5 h-5" />
</button>
</div>
</div>
</div>
{/* Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{loading && !dashboard ? (
<DashboardSkeleton />
) : error && !dashboard ? (
<ErrorState message={error} onRetry={fetchDashboard} />
) : selectedAssignment ? (
<DetailView
assignment={selectedAssignment}
onBack={handleBackToDashboard}
onUpdate={fetchDashboard}
addToast={addToast}
/>
) : dashboard ? (
<div className="space-y-6">
{/* Stats Row */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="Aktive Mandanten" value={dashboard.active_assignments} icon={<IconUsers />} />
<StatCard title="Stunden diesen Monat" value={`${dashboard.total_hours_this_month}h`} icon={<IconClock />} />
<StatCard title="Offene Aufgaben" value={dashboard.open_tasks} icon={<IconTask />} />
<StatCard title="Dringende Aufgaben" value={dashboard.urgent_tasks} icon={<IconAlert />}
accent={dashboard.urgent_tasks > 0} />
</div>
{/* Mandanten Grid */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Mandanten ({dashboard.total_assignments})</h2>
</div>
{dashboard.assignments.length === 0 ? (
<EmptyState icon={<IconUsers className="w-7 h-7" />} title="Keine Mandanten"
description="Es sind noch keine Mandanten-Zuweisungen vorhanden." />
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{dashboard.assignments.map((a) => (
<MandantCard key={a.id} assignment={a} onClick={() => setSelectedAssignment(a)} />
))}
</div>
)}
</div>
</div>
) : null}
</div>
{/* Inline animation styles */}
<style jsx global>{`
@keyframes slideIn {
from { opacity: 0; transform: translateX(100px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-slide-in { animation: slideIn 0.3s ease-out; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
`}</style>
</div>
)
}