- /sdk/consent: Replace hardcoded mockDocuments with GET /api/admin/consent/documents - /sdk/dsr: Replace createMockDSRList with fetchSDKDSRList via /api/sdk/v1/dsgvo/dsr - /sdk/dsr/new: Replace console.log mock with real POST to create DSR requests - /sdk/dsr/[requestId]: Replace mock lookup with real GET/PUT for DSR details and status updates - /sdk/consent-management: Add real stats, GDPR process counts, and email template editor - lib/sdk/dsr/api.ts: Add transformBackendDSR adapter (flat backend → nested frontend types) Prepares for removal of /dsgvo and /compliance pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
593 lines
23 KiB
TypeScript
593 lines
23 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect, useMemo } from 'react'
|
|
import Link from 'next/link'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
|
import {
|
|
DSRRequest,
|
|
DSRType,
|
|
DSRStatus,
|
|
DSRStatistics,
|
|
DSR_TYPE_INFO,
|
|
DSR_STATUS_INFO,
|
|
getDaysRemaining,
|
|
isOverdue,
|
|
isUrgent
|
|
} from '@/lib/sdk/dsr/types'
|
|
import { fetchSDKDSRList } from '@/lib/sdk/dsr/api'
|
|
import { DSRWorkflowStepperCompact } from '@/components/sdk/dsr'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
type TabId = 'overview' | 'intake' | 'processing' | 'completed' | 'settings'
|
|
|
|
interface Tab {
|
|
id: TabId
|
|
label: string
|
|
count?: number
|
|
countColor?: string
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMPONENTS
|
|
// =============================================================================
|
|
|
|
function TabNavigation({
|
|
tabs,
|
|
activeTab,
|
|
onTabChange
|
|
}: {
|
|
tabs: Tab[]
|
|
activeTab: TabId
|
|
onTabChange: (tab: TabId) => void
|
|
}) {
|
|
return (
|
|
<div className="border-b border-gray-200">
|
|
<nav className="flex gap-1 -mb-px" aria-label="Tabs">
|
|
{tabs.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => onTabChange(tab.id)}
|
|
className={`
|
|
px-4 py-3 text-sm font-medium border-b-2 transition-colors
|
|
${activeTab === tab.id
|
|
? 'border-purple-600 text-purple-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}
|
|
`}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
{tab.label}
|
|
{tab.count !== undefined && tab.count > 0 && (
|
|
<span className={`
|
|
px-2 py-0.5 text-xs rounded-full
|
|
${tab.countColor || 'bg-gray-100 text-gray-600'}
|
|
`}>
|
|
{tab.count}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StatCard({
|
|
label,
|
|
value,
|
|
color = 'gray',
|
|
icon,
|
|
trend
|
|
}: {
|
|
label: string
|
|
value: number | string
|
|
color?: 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'purple'
|
|
icon?: React.ReactNode
|
|
trend?: { value: number; label: string }
|
|
}) {
|
|
const colorClasses = {
|
|
gray: 'border-gray-200 text-gray-900',
|
|
blue: 'border-blue-200 text-blue-600',
|
|
yellow: 'border-yellow-200 text-yellow-600',
|
|
red: 'border-red-200 text-red-600',
|
|
green: 'border-green-200 text-green-600',
|
|
purple: 'border-purple-200 text-purple-600'
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-xl border ${colorClasses[color]} p-6`}>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className={`text-sm ${color === 'gray' ? 'text-gray-500' : `text-${color}-600`}`}>
|
|
{label}
|
|
</div>
|
|
<div className={`text-3xl font-bold mt-1 ${colorClasses[color].split(' ')[1]}`}>
|
|
{value}
|
|
</div>
|
|
{trend && (
|
|
<div className={`text-xs mt-1 ${trend.value >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
{trend.value >= 0 ? '+' : ''}{trend.value} {trend.label}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{icon && (
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center bg-${color}-50`}>
|
|
{icon}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function RequestCard({ request }: { request: DSRRequest }) {
|
|
const typeInfo = DSR_TYPE_INFO[request.type]
|
|
const statusInfo = DSR_STATUS_INFO[request.status]
|
|
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
|
|
const overdue = isOverdue(request)
|
|
const urgent = isUrgent(request)
|
|
|
|
return (
|
|
<Link href={`/sdk/dsr/${request.id}`}>
|
|
<div className={`
|
|
bg-white rounded-xl border-2 p-6 hover:shadow-md transition-all cursor-pointer
|
|
${overdue ? 'border-red-300 hover:border-red-400' :
|
|
urgent ? 'border-orange-300 hover:border-orange-400' :
|
|
request.status === 'completed' ? 'border-green-200 hover:border-green-300' :
|
|
'border-gray-200 hover:border-purple-300'
|
|
}
|
|
`}>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
{/* Header Badges */}
|
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
|
<span className="text-xs text-gray-500 font-mono">
|
|
{request.referenceNumber}
|
|
</span>
|
|
<span className={`px-2 py-1 text-xs rounded-full ${typeInfo.bgColor} ${typeInfo.color}`}>
|
|
{typeInfo.article} {typeInfo.labelShort}
|
|
</span>
|
|
{!request.identityVerification.verified && request.status !== 'completed' && request.status !== 'rejected' && (
|
|
<span className="px-2 py-1 text-xs bg-yellow-100 text-yellow-700 rounded-full flex items-center gap-1">
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
ID fehlt
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Requester Info */}
|
|
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
|
{request.requester.name}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 truncate">{request.requester.email}</p>
|
|
|
|
{/* Workflow Status */}
|
|
<div className="mt-3">
|
|
<DSRWorkflowStepperCompact currentStatus={request.status} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side - Deadline */}
|
|
<div className={`text-right ml-4 ${
|
|
overdue ? 'text-red-600' :
|
|
urgent ? 'text-orange-600' :
|
|
'text-gray-500'
|
|
}`}>
|
|
<div className="text-sm font-medium">
|
|
{request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled'
|
|
? statusInfo.label
|
|
: overdue
|
|
? `${Math.abs(daysRemaining)} Tage ueberfaellig`
|
|
: `${daysRemaining} Tage`
|
|
}
|
|
</div>
|
|
<div className="text-xs mt-0.5">
|
|
{new Date(request.receivedAt).toLocaleDateString('de-DE')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes Preview */}
|
|
{request.notes && (
|
|
<div className="mt-3 p-3 bg-gray-50 rounded-lg text-sm text-gray-600 line-clamp-2">
|
|
{request.notes}
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
|
|
<div className="text-sm text-gray-500">
|
|
{request.assignment.assignedTo
|
|
? `Zugewiesen: ${request.assignment.assignedTo}`
|
|
: 'Nicht zugewiesen'
|
|
}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{request.status !== 'completed' && request.status !== 'rejected' && request.status !== 'cancelled' && (
|
|
<>
|
|
{!request.identityVerification.verified && (
|
|
<span className="px-3 py-1 text-sm bg-yellow-50 text-yellow-700 rounded-lg">
|
|
ID pruefen
|
|
</span>
|
|
)}
|
|
<span className="px-3 py-1 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors">
|
|
Bearbeiten
|
|
</span>
|
|
</>
|
|
)}
|
|
{request.status === 'completed' && (
|
|
<span className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
|
Details
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
function FilterBar({
|
|
selectedType,
|
|
selectedStatus,
|
|
selectedPriority,
|
|
onTypeChange,
|
|
onStatusChange,
|
|
onPriorityChange,
|
|
onClear
|
|
}: {
|
|
selectedType: DSRType | 'all'
|
|
selectedStatus: DSRStatus | 'all'
|
|
selectedPriority: string
|
|
onTypeChange: (type: DSRType | 'all') => void
|
|
onStatusChange: (status: DSRStatus | 'all') => void
|
|
onPriorityChange: (priority: string) => void
|
|
onClear: () => void
|
|
}) {
|
|
const hasFilters = selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all'
|
|
|
|
return (
|
|
<div className="flex items-center gap-4 flex-wrap">
|
|
<span className="text-sm text-gray-500">Filter:</span>
|
|
|
|
{/* Type Filter */}
|
|
<select
|
|
value={selectedType}
|
|
onChange={(e) => onTypeChange(e.target.value as DSRType | 'all')}
|
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
|
>
|
|
<option value="all">Alle Typen</option>
|
|
{Object.entries(DSR_TYPE_INFO).map(([type, info]) => (
|
|
<option key={type} value={type}>{info.article} - {info.labelShort}</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* Status Filter */}
|
|
<select
|
|
value={selectedStatus}
|
|
onChange={(e) => onStatusChange(e.target.value as DSRStatus | 'all')}
|
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
|
>
|
|
<option value="all">Alle Status</option>
|
|
{Object.entries(DSR_STATUS_INFO).map(([status, info]) => (
|
|
<option key={status} value={status}>{info.label}</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* Priority Filter */}
|
|
<select
|
|
value={selectedPriority}
|
|
onChange={(e) => onPriorityChange(e.target.value)}
|
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
|
>
|
|
<option value="all">Alle Prioritaeten</option>
|
|
<option value="critical">Kritisch</option>
|
|
<option value="high">Hoch</option>
|
|
<option value="normal">Normal</option>
|
|
<option value="low">Niedrig</option>
|
|
</select>
|
|
|
|
{/* Clear Filters */}
|
|
{hasFilters && (
|
|
<button
|
|
onClick={onClear}
|
|
className="px-3 py-1.5 text-sm text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
Filter zuruecksetzen
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN PAGE
|
|
// =============================================================================
|
|
|
|
export default function DSRPage() {
|
|
const { state } = useSDK()
|
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
const [requests, setRequests] = useState<DSRRequest[]>([])
|
|
const [statistics, setStatistics] = useState<DSRStatistics | null>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
// Filters
|
|
const [selectedType, setSelectedType] = useState<DSRType | 'all'>('all')
|
|
const [selectedStatus, setSelectedStatus] = useState<DSRStatus | 'all'>('all')
|
|
const [selectedPriority, setSelectedPriority] = useState<string>('all')
|
|
|
|
// Load data from SDK backend
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const { requests: dsrRequests, statistics: dsrStats } = await fetchSDKDSRList()
|
|
setRequests(dsrRequests)
|
|
setStatistics(dsrStats)
|
|
} catch (error) {
|
|
console.error('Failed to load DSR data:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
loadData()
|
|
}, [])
|
|
|
|
// Calculate tab counts
|
|
const tabCounts = useMemo(() => {
|
|
return {
|
|
intake: requests.filter(r => r.status === 'intake' || r.status === 'identity_verification').length,
|
|
processing: requests.filter(r => r.status === 'processing').length,
|
|
completed: requests.filter(r => r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled').length,
|
|
overdue: requests.filter(r => isOverdue(r)).length
|
|
}
|
|
}, [requests])
|
|
|
|
// Filter requests based on active tab and filters
|
|
const filteredRequests = useMemo(() => {
|
|
let filtered = [...requests]
|
|
|
|
// Tab-based filtering
|
|
if (activeTab === 'intake') {
|
|
filtered = filtered.filter(r => r.status === 'intake' || r.status === 'identity_verification')
|
|
} else if (activeTab === 'processing') {
|
|
filtered = filtered.filter(r => r.status === 'processing')
|
|
} else if (activeTab === 'completed') {
|
|
filtered = filtered.filter(r => r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled')
|
|
}
|
|
|
|
// Type filter
|
|
if (selectedType !== 'all') {
|
|
filtered = filtered.filter(r => r.type === selectedType)
|
|
}
|
|
|
|
// Status filter
|
|
if (selectedStatus !== 'all') {
|
|
filtered = filtered.filter(r => r.status === selectedStatus)
|
|
}
|
|
|
|
// Priority filter
|
|
if (selectedPriority !== 'all') {
|
|
filtered = filtered.filter(r => r.priority === selectedPriority)
|
|
}
|
|
|
|
// Sort by urgency
|
|
return filtered.sort((a, b) => {
|
|
const getUrgency = (r: DSRRequest) => {
|
|
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return 100
|
|
const days = getDaysRemaining(r.deadline.currentDeadline)
|
|
if (days < 0) return -100 + days // Overdue items first
|
|
return days
|
|
}
|
|
return getUrgency(a) - getUrgency(b)
|
|
})
|
|
}, [requests, activeTab, selectedType, selectedStatus, selectedPriority])
|
|
|
|
const tabs: Tab[] = [
|
|
{ id: 'overview', label: 'Uebersicht' },
|
|
{ id: 'intake', label: 'Eingang', count: tabCounts.intake, countColor: 'bg-blue-100 text-blue-600' },
|
|
{ id: 'processing', label: 'In Bearbeitung', count: tabCounts.processing, countColor: 'bg-yellow-100 text-yellow-600' },
|
|
{ id: 'completed', label: 'Abgeschlossen', count: tabCounts.completed, countColor: 'bg-green-100 text-green-600' },
|
|
{ id: 'settings', label: 'Einstellungen' }
|
|
]
|
|
|
|
const stepInfo = STEP_EXPLANATIONS['dsr']
|
|
|
|
const clearFilters = () => {
|
|
setSelectedType('all')
|
|
setSelectedStatus('all')
|
|
setSelectedPriority('all')
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Step Header */}
|
|
<StepHeader
|
|
stepId="dsr"
|
|
title={stepInfo.title}
|
|
description={stepInfo.description}
|
|
explanation={stepInfo.explanation}
|
|
tips={stepInfo.tips}
|
|
>
|
|
<Link
|
|
href="/sdk/dsr/new"
|
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Anfrage erfassen
|
|
</Link>
|
|
</StepHeader>
|
|
|
|
{/* Tab Navigation */}
|
|
<TabNavigation
|
|
tabs={tabs}
|
|
activeTab={activeTab}
|
|
onTabChange={setActiveTab}
|
|
/>
|
|
|
|
{/* Loading State */}
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<svg className="animate-spin w-8 h-8 text-purple-600" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
</div>
|
|
) : activeTab === 'settings' ? (
|
|
/* Settings Tab */
|
|
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
|
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900">Einstellungen</h3>
|
|
<p className="mt-2 text-gray-500">
|
|
DSR-Portal-Einstellungen, E-Mail-Vorlagen und Workflow-Konfiguration
|
|
werden in einer spaeteren Version verfuegbar sein.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Statistics (Overview Tab) */}
|
|
{activeTab === 'overview' && statistics && (
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<StatCard
|
|
label="Gesamt"
|
|
value={statistics.total}
|
|
color="gray"
|
|
/>
|
|
<StatCard
|
|
label="Neue Anfragen"
|
|
value={statistics.byStatus.intake + statistics.byStatus.identity_verification}
|
|
color="blue"
|
|
/>
|
|
<StatCard
|
|
label="In Bearbeitung"
|
|
value={statistics.byStatus.processing}
|
|
color="yellow"
|
|
/>
|
|
<StatCard
|
|
label="Ueberfaellig"
|
|
value={tabCounts.overdue}
|
|
color={tabCounts.overdue > 0 ? 'red' : 'green'}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Overdue Alert */}
|
|
{tabCounts.overdue > 0 && (
|
|
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex items-center gap-4">
|
|
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1">
|
|
<h4 className="font-medium text-red-800">
|
|
Achtung: {tabCounts.overdue} ueberfaellige Anfrage(n)
|
|
</h4>
|
|
<p className="text-sm text-red-600">
|
|
Die gesetzliche Frist ist abgelaufen. Handeln Sie umgehend, um Bussgelder zu vermeiden.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setActiveTab('overview')
|
|
setSelectedStatus('all')
|
|
}}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium"
|
|
>
|
|
Anzeigen
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Info Box (Overview Tab) */}
|
|
{activeTab === 'overview' && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
|
<div className="flex items-start gap-3">
|
|
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div>
|
|
<h4 className="font-medium text-blue-800">Fristen beachten</h4>
|
|
<p className="text-sm text-blue-600 mt-1">
|
|
Nach Art. 12 DSGVO muessen Anfragen innerhalb von einem Monat beantwortet werden.
|
|
Eine Verlaengerung um zwei weitere Monate ist bei komplexen Anfragen moeglich,
|
|
sofern der Betroffene innerhalb eines Monats darueber informiert wird.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Filters */}
|
|
<FilterBar
|
|
selectedType={selectedType}
|
|
selectedStatus={selectedStatus}
|
|
selectedPriority={selectedPriority}
|
|
onTypeChange={setSelectedType}
|
|
onStatusChange={setSelectedStatus}
|
|
onPriorityChange={setSelectedPriority}
|
|
onClear={clearFilters}
|
|
/>
|
|
|
|
{/* Requests List */}
|
|
<div className="space-y-4">
|
|
{filteredRequests.map(request => (
|
|
<RequestCard key={request.id} request={request} />
|
|
))}
|
|
</div>
|
|
|
|
{/* Empty State */}
|
|
{filteredRequests.length === 0 && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
|
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900">Keine Anfragen gefunden</h3>
|
|
<p className="mt-2 text-gray-500">
|
|
{selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all'
|
|
? 'Passen Sie die Filter an oder'
|
|
: 'Es sind noch keine Anfragen vorhanden.'
|
|
}
|
|
</p>
|
|
{(selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all') ? (
|
|
<button
|
|
onClick={clearFilters}
|
|
className="mt-4 px-4 py-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
|
|
>
|
|
Filter zuruecksetzen
|
|
</button>
|
|
) : (
|
|
<Link
|
|
href="/sdk/dsr/new"
|
|
className="mt-4 inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Erste Anfrage erfassen
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|