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>
136 lines
5.3 KiB
TypeScript
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>
|
|
)
|
|
}
|