Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
264 lines
9.8 KiB
TypeScript
264 lines
9.8 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import {
|
|
IdentityVerificationMethod,
|
|
DSRVerifyIdentityRequest
|
|
} from '@/lib/sdk/dsr/types'
|
|
|
|
interface DSRIdentityModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
onVerify: (verification: DSRVerifyIdentityRequest) => Promise<void>
|
|
requesterName: string
|
|
requesterEmail: string
|
|
}
|
|
|
|
const VERIFICATION_METHODS: {
|
|
value: IdentityVerificationMethod
|
|
label: string
|
|
description: string
|
|
icon: string
|
|
}[] = [
|
|
{
|
|
value: 'id_document',
|
|
label: 'Ausweisdokument',
|
|
description: 'Kopie von Personalausweis oder Reisepass',
|
|
icon: 'M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2'
|
|
},
|
|
{
|
|
value: 'email',
|
|
label: 'E-Mail-Bestaetigung',
|
|
description: 'Bestaetigung ueber verifizierte E-Mail-Adresse',
|
|
icon: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'
|
|
},
|
|
{
|
|
value: 'existing_account',
|
|
label: 'Bestehendes Konto',
|
|
description: 'Anmeldung ueber bestehendes Kundenkonto',
|
|
icon: 'M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
},
|
|
{
|
|
value: 'phone',
|
|
label: 'Telefonische Bestaetigung',
|
|
description: 'Verifizierung per Telefonanruf',
|
|
icon: 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z'
|
|
},
|
|
{
|
|
value: 'postal',
|
|
label: 'Postalische Bestaetigung',
|
|
description: 'Bestaetigung per Brief',
|
|
icon: 'M3 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5m0 0l-1.14.76a2 2 0 01-2.22 0l-1.14-.76'
|
|
},
|
|
{
|
|
value: 'other',
|
|
label: 'Sonstige Methode',
|
|
description: 'Andere Verifizierungsmethode',
|
|
icon: 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
}
|
|
]
|
|
|
|
export function DSRIdentityModal({
|
|
isOpen,
|
|
onClose,
|
|
onVerify,
|
|
requesterName,
|
|
requesterEmail
|
|
}: DSRIdentityModalProps) {
|
|
const [selectedMethod, setSelectedMethod] = useState<IdentityVerificationMethod | null>(null)
|
|
const [notes, setNotes] = useState('')
|
|
const [documentRef, setDocumentRef] = useState('')
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
if (!isOpen) return null
|
|
|
|
const handleVerify = async () => {
|
|
if (!selectedMethod) {
|
|
setError('Bitte waehlen Sie eine Verifizierungsmethode')
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
await onVerify({
|
|
method: selectedMethod,
|
|
notes: notes || undefined,
|
|
documentRef: documentRef || undefined
|
|
})
|
|
onClose()
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Verifizierung fehlgeschlagen')
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleClose = () => {
|
|
setSelectedMethod(null)
|
|
setNotes('')
|
|
setDocumentRef('')
|
|
setError(null)
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/50"
|
|
onClick={handleClose}
|
|
/>
|
|
|
|
{/* Modal */}
|
|
<div className="relative bg-white rounded-2xl shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-hidden">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
Identitaet verifizieren
|
|
</h2>
|
|
<button
|
|
onClick={handleClose}
|
|
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="px-6 py-4 overflow-y-auto max-h-[60vh]">
|
|
{/* Requester Info */}
|
|
<div className="bg-gray-50 rounded-xl p-4 mb-6">
|
|
<div className="text-sm text-gray-500 mb-1">Antragsteller</div>
|
|
<div className="font-medium text-gray-900">{requesterName}</div>
|
|
<div className="text-sm text-gray-600">{requesterEmail}</div>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Verification Methods */}
|
|
<div className="space-y-2 mb-6">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Verifizierungsmethode
|
|
</label>
|
|
{VERIFICATION_METHODS.map(method => (
|
|
<button
|
|
key={method.value}
|
|
onClick={() => setSelectedMethod(method.value)}
|
|
className={`
|
|
w-full flex items-start gap-3 p-3 rounded-xl border-2 text-left transition-all
|
|
${selectedMethod === method.value
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
|
}
|
|
`}
|
|
>
|
|
<div className={`
|
|
w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0
|
|
${selectedMethod === method.value ? 'bg-purple-100' : 'bg-gray-100'}
|
|
`}>
|
|
<svg
|
|
className={`w-5 h-5 ${selectedMethod === method.value ? 'text-purple-600' : 'text-gray-500'}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={method.icon} />
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className={`font-medium ${selectedMethod === method.value ? 'text-purple-700' : 'text-gray-900'}`}>
|
|
{method.label}
|
|
</div>
|
|
<div className="text-sm text-gray-500">
|
|
{method.description}
|
|
</div>
|
|
</div>
|
|
{selectedMethod === method.value && (
|
|
<svg className="w-5 h-5 text-purple-600 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Document Reference */}
|
|
{selectedMethod === 'id_document' && (
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Dokumentenreferenz (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={documentRef}
|
|
onChange={(e) => setDocumentRef(e.target.value)}
|
|
placeholder="z.B. Datei-ID oder Speicherort"
|
|
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>
|
|
)}
|
|
|
|
{/* Notes */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Notizen (optional)
|
|
</label>
|
|
<textarea
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
placeholder="Weitere Informationen zur Verifizierung..."
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-end gap-3">
|
|
<button
|
|
onClick={handleClose}
|
|
className="px-4 py-2 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={handleVerify}
|
|
disabled={!selectedMethod || isLoading}
|
|
className={`
|
|
px-4 py-2 rounded-lg font-medium transition-colors
|
|
${selectedMethod && !isLoading
|
|
? 'bg-green-600 text-white hover:bg-green-700'
|
|
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
|
}
|
|
`}
|
|
>
|
|
{isLoading ? (
|
|
<span className="flex items-center gap-2">
|
|
<svg className="animate-spin w-4 h-4" 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>
|
|
Verifiziere...
|
|
</span>
|
|
) : (
|
|
'Identitaet bestaetigen'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|