refactor(admin): split loeschfristen and vvt pages
Reduce both page.tsx files below the 500-LOC hard cap by extracting all inline tab components and API helpers into colocated _components/. - loeschfristen/page.tsx: 2720 → 467 LOC - vvt/page.tsx: 2297 → 256 LOC New files: LoeschkonzeptTab, loeschfristen/api, TabDokument, TabProcessor Updated: TabVerzeichnis (template picker + badge), vvt/api (template helpers) Fixed: VVTLinkSection wrong field name (linkedVVTActivityIds), VendorLinkSection added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,14 @@ import {
|
||||
generateActivities,
|
||||
prefillFromScopeAnswers,
|
||||
} from '@/lib/sdk/vvt-profiling'
|
||||
import { apiListTemplates, type ProcessTemplate } from './api'
|
||||
|
||||
const PROTECTION_LEVEL_LABELS: Record<string, string> = { LOW: 'Niedrig', MEDIUM: 'Mittel', HIGH: 'Hoch', VERY_HIGH: 'Sehr hoch' }
|
||||
|
||||
export function TabVerzeichnis({
|
||||
activities, allActivities, activeCount, draftCount, thirdCountryCount, art9Count,
|
||||
filter, setFilter, searchQuery, setSearchQuery, sortBy, setSortBy,
|
||||
scopeAnswers, onEdit, onNew, onDelete, onAdoptGenerated,
|
||||
scopeAnswers, onEdit, onNew, onDelete, onAdoptGenerated, onNewFromTemplate,
|
||||
}: {
|
||||
activities: VVTActivity[]
|
||||
allActivities: VVTActivity[]
|
||||
@@ -35,9 +38,14 @@ export function TabVerzeichnis({
|
||||
onNew: () => void
|
||||
onDelete: (id: string) => void
|
||||
onAdoptGenerated: (activities: VVTActivity[]) => void
|
||||
onNewFromTemplate: (templateId: string) => void
|
||||
}) {
|
||||
const [scopePreview, setScopePreview] = useState<VVTActivity[] | null>(null)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [showTemplatePicker, setShowTemplatePicker] = useState(false)
|
||||
const [templates, setTemplates] = useState<ProcessTemplate[]>([])
|
||||
const [templateFilter, setTemplateFilter] = useState<string>('all')
|
||||
const [templatesLoading, setTemplatesLoading] = useState(false)
|
||||
|
||||
const handleGenerateFromScope = useCallback(() => {
|
||||
if (!scopeAnswers) return
|
||||
@@ -176,6 +184,26 @@ export function TabVerzeichnis({
|
||||
<option value="date">Datum</option>
|
||||
<option value="status">Status</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setShowTemplatePicker(true)
|
||||
if (templates.length === 0) {
|
||||
setTemplatesLoading(true)
|
||||
try {
|
||||
const t = await apiListTemplates()
|
||||
setTemplates(t)
|
||||
} finally {
|
||||
setTemplatesLoading(false)
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors text-sm whitespace-nowrap"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414A1 1 0 0121 8.414V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
|
||||
</svg>
|
||||
Aus Vorlage
|
||||
</button>
|
||||
<button
|
||||
onClick={onNew}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm whitespace-nowrap"
|
||||
@@ -207,6 +235,79 @@ export function TabVerzeichnis({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template Picker Modal */}
|
||||
{showTemplatePicker && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Vorlage auswaehlen</h3>
|
||||
<button onClick={() => setShowTemplatePicker(false)} className="text-gray-400 hover:text-gray-600">
|
||||
<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 className="flex gap-2 mt-4 flex-wrap">
|
||||
{['all', 'hr', 'it', 'marketing', 'finance', 'legal', 'operations'].map(f => (
|
||||
<button key={f} onClick={() => setTemplateFilter(f)}
|
||||
className={`px-3 py-1 text-xs rounded-full ${templateFilter === f ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}>
|
||||
{f === 'all' ? 'Alle' : f.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{templatesLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{templates
|
||||
.filter(t => templateFilter === 'all' || t.business_function === templateFilter)
|
||||
.map(t => (
|
||||
<button key={t.id} onClick={() => { onNewFromTemplate(t.id); setShowTemplatePicker(false) }}
|
||||
className="w-full text-left p-4 border border-gray-200 rounded-xl hover:border-indigo-300 hover:bg-indigo-50 transition-colors">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span className="text-sm font-medium text-gray-900">{t.name}</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-indigo-100 text-indigo-700 rounded-full">{t.business_function.toUpperCase()}</span>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${PROTECTION_LEVEL_LABELS[t.protection_level] ? 'bg-gray-100 text-gray-600' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{PROTECTION_LEVEL_LABELS[t.protection_level] || t.protection_level}
|
||||
</span>
|
||||
{t.dpia_required && (
|
||||
<span className="px-2 py-0.5 text-xs bg-purple-100 text-purple-700 rounded-full">DSFA</span>
|
||||
)}
|
||||
</div>
|
||||
{t.description && <p className="text-xs text-gray-500 line-clamp-1">{t.description}</p>}
|
||||
{t.tags.length > 0 && (
|
||||
<div className="flex gap-1 mt-1 flex-wrap">
|
||||
{t.tags.slice(0, 3).map(tag => (
|
||||
<span key={tag} className="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-500 rounded">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{t.risk_score !== undefined && (
|
||||
<span className={`text-xs font-medium px-2 py-1 rounded-lg whitespace-nowrap ${
|
||||
t.risk_score >= 7 ? 'bg-red-100 text-red-700' : t.risk_score >= 4 ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'
|
||||
}`}>Risiko {t.risk_score}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{templates.filter(t => templateFilter === 'all' || t.business_function === templateFilter).length === 0 && (
|
||||
<p className="text-center text-gray-500 py-8 text-sm">Keine Vorlagen fuer diesen Bereich gefunden.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -248,6 +349,9 @@ function ActivityCard({ activity, onEdit, onDelete }: { activity: VVTActivity; o
|
||||
{activity.dpiaRequired && (
|
||||
<span className="px-2 py-0.5 text-xs bg-purple-100 text-purple-700 rounded-full">DSFA</span>
|
||||
)}
|
||||
{(activity as any).sourceTemplateId && (
|
||||
<span className="px-2 py-0.5 text-xs bg-indigo-100 text-indigo-700 rounded-full">Vorlage</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-gray-900 truncate">{activity.name || '(Ohne Namen)'}</h3>
|
||||
{activity.description && (
|
||||
@@ -257,6 +361,12 @@ function ActivityCard({ activity, onEdit, onDelete }: { activity: VVTActivity; o
|
||||
<span>{BUSINESS_FUNCTION_LABELS[activity.businessFunction]}</span>
|
||||
<span>{activity.responsible || 'Kein Verantwortlicher'}</span>
|
||||
<span>Aktualisiert: {new Date(activity.updatedAt).toLocaleDateString('de-DE')}</span>
|
||||
{(activity as any).art30Completeness !== undefined && (
|
||||
<span className={`font-medium ${
|
||||
(activity as any).art30Completeness >= 80 ? 'text-green-600' :
|
||||
(activity as any).art30Completeness >= 50 ? 'text-yellow-600' : 'text-red-500'
|
||||
}`}>Art.30: {(activity as any).art30Completeness}%</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
|
||||
Reference in New Issue
Block a user