fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
464
admin-v2/components/sdk/CommandBar/CommandBar.tsx
Normal file
464
admin-v2/components/sdk/CommandBar/CommandBar.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSDK, SDK_STEPS, CommandType, CommandHistory, downloadExport } from '@/lib/sdk'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
interface Suggestion {
|
||||
id: string
|
||||
type: CommandType
|
||||
label: string
|
||||
description: string
|
||||
shortcut?: string
|
||||
icon: React.ReactNode
|
||||
action: () => void | Promise<void>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ICONS
|
||||
// =============================================================================
|
||||
|
||||
const icons = {
|
||||
navigation: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
),
|
||||
action: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
),
|
||||
search: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
),
|
||||
generate: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
),
|
||||
help: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMAND BAR
|
||||
// =============================================================================
|
||||
|
||||
interface CommandBarProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function CommandBar({ onClose }: CommandBarProps) {
|
||||
const router = useRouter()
|
||||
const { state, dispatch, goToStep } = useSDK()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Focus input on mount
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
// Generate suggestions based on query
|
||||
const suggestions = useMemo((): Suggestion[] => {
|
||||
const results: Suggestion[] = []
|
||||
const lowerQuery = query.toLowerCase()
|
||||
|
||||
// Navigation suggestions
|
||||
SDK_STEPS.forEach(step => {
|
||||
const matchesName = step.name.toLowerCase().includes(lowerQuery) ||
|
||||
step.nameShort.toLowerCase().includes(lowerQuery)
|
||||
const matchesDescription = step.description.toLowerCase().includes(lowerQuery)
|
||||
|
||||
if (!query || matchesName || matchesDescription) {
|
||||
results.push({
|
||||
id: `nav-${step.id}`,
|
||||
type: 'NAVIGATION',
|
||||
label: `Gehe zu ${step.name}`,
|
||||
description: step.description,
|
||||
icon: icons.navigation,
|
||||
action: () => {
|
||||
goToStep(step.id)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Action suggestions
|
||||
const actions: Suggestion[] = [
|
||||
{
|
||||
id: 'action-new-usecase',
|
||||
type: 'ACTION',
|
||||
label: 'Neuen Anwendungsfall erstellen',
|
||||
description: 'Startet die Anwendungsfall-Erfassung',
|
||||
icon: icons.action,
|
||||
action: () => {
|
||||
goToStep('use-case-assessment')
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'action-export-pdf',
|
||||
type: 'ACTION',
|
||||
label: 'Als PDF exportieren',
|
||||
description: 'Exportiert Compliance-Bericht als PDF',
|
||||
icon: icons.action,
|
||||
action: async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await downloadExport(state, 'pdf')
|
||||
} catch (error) {
|
||||
console.error('PDF export failed:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'action-export-zip',
|
||||
type: 'ACTION',
|
||||
label: 'Als ZIP exportieren',
|
||||
description: 'Exportiert alle Daten und Dokumente als ZIP-Archiv',
|
||||
icon: icons.action,
|
||||
action: async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await downloadExport(state, 'zip')
|
||||
} catch (error) {
|
||||
console.error('ZIP export failed:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'action-export-json',
|
||||
type: 'ACTION',
|
||||
label: 'Als JSON exportieren',
|
||||
description: 'Exportiert den kompletten State als JSON',
|
||||
icon: icons.action,
|
||||
action: async () => {
|
||||
try {
|
||||
await downloadExport(state, 'json')
|
||||
} catch (error) {
|
||||
console.error('JSON export failed:', error)
|
||||
} finally {
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'action-validate',
|
||||
type: 'ACTION',
|
||||
label: 'Checkpoint validieren',
|
||||
description: 'Validiert den aktuellen Schritt',
|
||||
icon: icons.action,
|
||||
action: () => {
|
||||
// Trigger validation
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
actions.forEach(action => {
|
||||
if (!query || action.label.toLowerCase().includes(lowerQuery)) {
|
||||
results.push(action)
|
||||
}
|
||||
})
|
||||
|
||||
// Generate suggestions
|
||||
const generateSuggestions: Suggestion[] = [
|
||||
{
|
||||
id: 'gen-dsfa',
|
||||
type: 'GENERATE',
|
||||
label: 'DSFA generieren',
|
||||
description: 'Generiert eine Datenschutz-Folgenabschätzung',
|
||||
icon: icons.generate,
|
||||
action: () => {
|
||||
goToStep('dsfa')
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gen-tom',
|
||||
type: 'GENERATE',
|
||||
label: 'TOMs generieren',
|
||||
description: 'Generiert technische und organisatorische Maßnahmen',
|
||||
icon: icons.generate,
|
||||
action: () => {
|
||||
goToStep('tom')
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gen-vvt',
|
||||
type: 'GENERATE',
|
||||
label: 'VVT generieren',
|
||||
description: 'Generiert das Verarbeitungsverzeichnis',
|
||||
icon: icons.generate,
|
||||
action: () => {
|
||||
goToStep('vvt')
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gen-cookie',
|
||||
type: 'GENERATE',
|
||||
label: 'Cookie Banner generieren',
|
||||
description: 'Generiert Cookie-Consent-Banner Code',
|
||||
icon: icons.generate,
|
||||
action: () => {
|
||||
goToStep('cookie-banner')
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
generateSuggestions.forEach(suggestion => {
|
||||
if (!query || suggestion.label.toLowerCase().includes(lowerQuery)) {
|
||||
results.push(suggestion)
|
||||
}
|
||||
})
|
||||
|
||||
// Help suggestions
|
||||
const helpSuggestions: Suggestion[] = [
|
||||
{
|
||||
id: 'help-docs',
|
||||
type: 'HELP',
|
||||
label: 'Dokumentation öffnen',
|
||||
description: 'Öffnet die SDK-Dokumentation',
|
||||
icon: icons.help,
|
||||
action: () => {
|
||||
window.open('/docs/sdk', '_blank')
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'help-next',
|
||||
type: 'HELP',
|
||||
label: 'Was muss ich als nächstes tun?',
|
||||
description: 'Zeigt den nächsten empfohlenen Schritt',
|
||||
icon: icons.help,
|
||||
action: () => {
|
||||
// Show contextual help
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
helpSuggestions.forEach(suggestion => {
|
||||
if (!query || suggestion.label.toLowerCase().includes(lowerQuery)) {
|
||||
results.push(suggestion)
|
||||
}
|
||||
})
|
||||
|
||||
return results.slice(0, 10)
|
||||
}, [query, state, goToStep, onClose])
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => Math.max(prev - 1, 0))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (suggestions[selectedIndex]) {
|
||||
executeSuggestion(suggestions[selectedIndex])
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
onClose()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [suggestions, selectedIndex, onClose])
|
||||
|
||||
// Reset selected index when query changes
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
}, [query])
|
||||
|
||||
const executeSuggestion = async (suggestion: Suggestion) => {
|
||||
// Add to history
|
||||
const historyEntry: CommandHistory = {
|
||||
id: `${Date.now()}`,
|
||||
query: suggestion.label,
|
||||
type: suggestion.type,
|
||||
timestamp: new Date(),
|
||||
success: true,
|
||||
}
|
||||
dispatch({ type: 'ADD_COMMAND_HISTORY', payload: historyEntry })
|
||||
|
||||
try {
|
||||
await suggestion.action()
|
||||
} catch (error) {
|
||||
console.error('Command execution failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: CommandType): string => {
|
||||
switch (type) {
|
||||
case 'NAVIGATION':
|
||||
return 'Navigation'
|
||||
case 'ACTION':
|
||||
return 'Aktion'
|
||||
case 'SEARCH':
|
||||
return 'Suche'
|
||||
case 'GENERATE':
|
||||
return 'Generieren'
|
||||
case 'HELP':
|
||||
return 'Hilfe'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative min-h-screen flex items-start justify-center pt-[15vh] px-4">
|
||||
<div className="relative w-full max-w-2xl bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
{/* Search Input */}
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b border-gray-200">
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="Suchen oder Befehl eingeben..."
|
||||
className="flex-1 text-lg bg-transparent border-none outline-none placeholder-gray-400"
|
||||
/>
|
||||
{isLoading && (
|
||||
<svg className="animate-spin w-5 h-5 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>
|
||||
)}
|
||||
<kbd className="px-2 py-1 text-xs text-gray-400 bg-gray-100 rounded">ESC</kbd>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
<div className="max-h-96 overflow-y-auto py-2">
|
||||
{suggestions.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center text-gray-500">
|
||||
Keine Ergebnisse für "{query}"
|
||||
</div>
|
||||
) : (
|
||||
suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
onClick={() => executeSuggestion(suggestion)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
|
||||
index === selectedIndex
|
||||
? 'bg-purple-50 text-purple-900'
|
||||
: 'text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
index === selectedIndex
|
||||
? 'bg-purple-100 text-purple-600'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{suggestion.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{suggestion.label}</div>
|
||||
<div className="text-sm text-gray-500 truncate">{suggestion.description}</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs rounded-full ${
|
||||
suggestion.type === 'NAVIGATION'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: suggestion.type === 'ACTION'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: suggestion.type === 'GENERATE'
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{getTypeLabel(suggestion.type)}
|
||||
</span>
|
||||
{suggestion.shortcut && (
|
||||
<kbd className="px-1.5 py-0.5 text-xs bg-gray-100 rounded">
|
||||
{suggestion.shortcut}
|
||||
</kbd>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-t border-gray-200 bg-gray-50 text-xs text-gray-500">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex items-center gap-1">
|
||||
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded">↑</kbd>
|
||||
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded">↓</kbd>
|
||||
navigieren
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded">↵</kbd>
|
||||
auswählen
|
||||
</span>
|
||||
</div>
|
||||
<span>AI Compliance SDK v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
admin-v2/components/sdk/CommandBar/index.ts
Normal file
1
admin-v2/components/sdk/CommandBar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CommandBar } from './CommandBar'
|
||||
Reference in New Issue
Block a user