Files
breakpilot-compliance/admin-compliance/app/sdk/dsr/new/page.tsx
Sharang Parnerkar e04816cfe5 refactor(admin): split dsr/new, compliance-hub, iace/monitoring, cookie-banner pages
Extract components and hooks from 4 oversized pages (518–508 LOC each) to bring
each page.tsx under 300 LOC (hard cap 500). Zero behavior changes.

- dsr/new: TypeSelector, SourceSelector → _components/; useNewDSRForm → _hooks/
- compliance-hub: QuickActions, StatsRow, DomainChart, MappingsAndFindings,
  RegulationsTable → _components/; useComplianceHub → _hooks/
- iace/[projectId]/monitoring: Badges, EventForm, ResolveModal, TimelineEvent →
  _components/; useMonitoring → _hooks/
- cookie-banner: BannerPreview, CategoryCard → _components/; useCookieBanner → _hooks/

Result: page.tsx LOC: dsr/new=259, compliance-hub=95, monitoring=157, cookie-banner=212

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 13:22:01 +02:00

260 lines
11 KiB
TypeScript

'use client'
import React from 'react'
import Link from 'next/link'
import { DSRType, DSRPriority } from '@/lib/sdk/dsr/types'
import { TypeSelector } from './_components/TypeSelector'
import { SourceSelector } from './_components/SourceSelector'
import { useNewDSRForm } from './_hooks/useNewDSRForm'
export default function NewDSRPage() {
const { formData, errors, isSubmitting, updateField, handleSubmit } = useNewDSRForm()
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Link
href="/sdk/dsr"
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg 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="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Neue Anfrage erfassen</h1>
<p className="text-gray-500 mt-1">
Erfassen Sie eine neue Betroffenenanfrage (Art. 15-21 DSGVO)
</p>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-8">
{/* Type Selection */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<TypeSelector
selectedType={formData.type}
onSelect={(type) => updateField('type', type)}
/>
{errors.type && (
<p className="mt-2 text-sm text-red-600">{errors.type}</p>
)}
</div>
{/* Requester Information */}
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-6">
<h2 className="text-lg font-semibold text-gray-900">Antragsteller</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.requesterName}
onChange={(e) => updateField('requesterName', e.target.value)}
placeholder="Max Mustermann"
className={`
w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500
${errors.requesterName ? 'border-red-300' : 'border-gray-300'}
`}
/>
{errors.requesterName && (
<p className="mt-1 text-sm text-red-600">{errors.requesterName}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
E-Mail <span className="text-red-500">*</span>
</label>
<input
type="email"
value={formData.requesterEmail}
onChange={(e) => updateField('requesterEmail', e.target.value)}
placeholder="max.mustermann@example.de"
className={`
w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500
${errors.requesterEmail ? 'border-red-300' : 'border-gray-300'}
`}
/>
{errors.requesterEmail && (
<p className="mt-1 text-sm text-red-600">{errors.requesterEmail}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Telefon (optional)
</label>
<input
type="tel"
value={formData.requesterPhone}
onChange={(e) => updateField('requesterPhone', e.target.value)}
placeholder="+49 170 1234567"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kunden-ID (optional)
</label>
<input
type="text"
value={formData.customerId}
onChange={(e) => updateField('customerId', e.target.value)}
placeholder="Falls bekannt"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Adresse (optional)
</label>
<textarea
value={formData.requesterAddress}
onChange={(e) => updateField('requesterAddress', e.target.value)}
placeholder="Strasse, PLZ, Ort"
rows={2}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-none"
/>
</div>
</div>
{/* Source Information */}
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-6">
<h2 className="text-lg font-semibold text-gray-900">Anfrage-Details</h2>
<SourceSelector
selectedSource={formData.source}
sourceDetails={formData.sourceDetails}
onSourceChange={(source) => updateField('source', source)}
onDetailsChange={(details) => updateField('sourceDetails', details)}
/>
{errors.source && (
<p className="mt-1 text-sm text-red-600">{errors.source}</p>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Anfrage-Text (optional)
</label>
<textarea
value={formData.requestText}
onChange={(e) => updateField('requestText', e.target.value)}
placeholder="Kopieren Sie hier den Text der Anfrage ein..."
rows={5}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-none"
/>
<p className="mt-1 text-xs text-gray-500">
Originaler Wortlaut der Anfrage fuer die Dokumentation
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Prioritaet
</label>
<div className="flex gap-2">
{[
{ value: 'low', label: 'Niedrig', color: 'bg-blue-100 text-blue-700 border-blue-200' },
{ value: 'normal', label: 'Normal', color: 'bg-gray-100 text-gray-700 border-gray-200' },
{ value: 'high', label: 'Hoch', color: 'bg-orange-100 text-orange-700 border-orange-200' },
{ value: 'critical', label: 'Kritisch', color: 'bg-red-100 text-red-700 border-red-200' }
].map(priority => (
<button
key={priority.value}
type="button"
onClick={() => updateField('priority', priority.value as DSRPriority)}
className={`
px-4 py-2 rounded-lg border-2 text-sm font-medium transition-all
${formData.priority === priority.value
? priority.color + ' border-current'
: 'bg-white text-gray-500 border-gray-200 hover:border-gray-300'
}
`}
>
{priority.label}
</button>
))}
</div>
</div>
</div>
{/* Submit Error */}
{errors.submit && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
<div className="flex items-center gap-3">
<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 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-red-700">{errors.submit}</p>
</div>
</div>
)}
{/* Actions */}
<div className="flex items-center justify-end gap-4">
<Link
href="/sdk/dsr"
className="px-6 py-2.5 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
>
Abbrechen
</Link>
<button
type="submit"
disabled={isSubmitting}
className={`
px-6 py-2.5 rounded-lg font-medium transition-colors flex items-center gap-2
${!isSubmitting
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}
`}
>
{isSubmitting ? (
<>
<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>
Wird erstellt...
</>
) : (
<>
<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
</>
)}
</button>
</div>
</form>
{/* Info Box */}
<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">Hinweis zur Eingangsbestaetigung</h4>
<p className="text-sm text-blue-700 mt-1">
Nach Erfassung der Anfrage wird automatisch eine Eingangsbestaetigung erstellt.
Sie koennen diese im naechsten Schritt an den Antragsteller senden.
Die gesetzliche Frist beginnt mit dem Eingangsdatum.
</p>
</div>
</div>
</div>
</div>
)
}