From 76b108a29f7ee6790399cbda5c70ae24c8070cf3 Mon Sep 17 00:00:00 2001 From: BreakPilot Dev Date: Tue, 10 Feb 2026 12:12:38 +0100 Subject: [PATCH 1/2] feat(admin-v2): Katalogverwaltung ins Admin-Dashboard integrieren Katalogverwaltung von /sdk/catalog-manager nach /dashboard/catalog-manager verschoben, damit sie im Admin-Dashboard mit Sidebar erscheint statt im SDK-Bereich. Shared Components extrahiert, SDK-Route bleibt funktionsfaehig. Co-Authored-By: Claude Opus 4.6 --- .../dashboard/catalog-manager/page.tsx | 12 + .../app/(sdk)/sdk/catalog-manager/page.tsx | 7 + .../catalog-manager/CatalogEntryForm.tsx | 392 +++++++++++ .../catalog-manager/CatalogManagerContent.tsx | 381 ++++++++++ .../catalog-manager/CatalogModuleTabs.tsx | 121 ++++ .../catalog-manager/CatalogTable.tsx | 254 +++++++ admin-v2/components/layout/Sidebar.tsx | 6 +- admin-v2/lib/navigation.ts | 32 +- .../sdk/catalog-manager/catalog-registry.ts | 665 ++++++++++++++++++ admin-v2/lib/sdk/catalog-manager/types.ts | 118 ++++ 10 files changed, 1985 insertions(+), 3 deletions(-) create mode 100644 admin-v2/app/(admin)/dashboard/catalog-manager/page.tsx create mode 100644 admin-v2/app/(sdk)/sdk/catalog-manager/page.tsx create mode 100644 admin-v2/components/catalog-manager/CatalogEntryForm.tsx create mode 100644 admin-v2/components/catalog-manager/CatalogManagerContent.tsx create mode 100644 admin-v2/components/catalog-manager/CatalogModuleTabs.tsx create mode 100644 admin-v2/components/catalog-manager/CatalogTable.tsx create mode 100644 admin-v2/lib/sdk/catalog-manager/catalog-registry.ts create mode 100644 admin-v2/lib/sdk/catalog-manager/types.ts diff --git a/admin-v2/app/(admin)/dashboard/catalog-manager/page.tsx b/admin-v2/app/(admin)/dashboard/catalog-manager/page.tsx new file mode 100644 index 0000000..9948c94 --- /dev/null +++ b/admin-v2/app/(admin)/dashboard/catalog-manager/page.tsx @@ -0,0 +1,12 @@ +'use client' + +import { SDKProvider } from '@/lib/sdk/context' +import { CatalogManagerContent } from '@/components/catalog-manager/CatalogManagerContent' + +export default function AdminCatalogManagerPage() { + return ( + + + + ) +} diff --git a/admin-v2/app/(sdk)/sdk/catalog-manager/page.tsx b/admin-v2/app/(sdk)/sdk/catalog-manager/page.tsx new file mode 100644 index 0000000..ee9d5be --- /dev/null +++ b/admin-v2/app/(sdk)/sdk/catalog-manager/page.tsx @@ -0,0 +1,7 @@ +'use client' + +import { CatalogManagerContent } from '@/components/catalog-manager/CatalogManagerContent' + +export default function CatalogManagerPage() { + return +} diff --git a/admin-v2/components/catalog-manager/CatalogEntryForm.tsx b/admin-v2/components/catalog-manager/CatalogEntryForm.tsx new file mode 100644 index 0000000..ab06c00 --- /dev/null +++ b/admin-v2/components/catalog-manager/CatalogEntryForm.tsx @@ -0,0 +1,392 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { X } from 'lucide-react' +import type { + CatalogMeta, + CatalogEntry, + CatalogFieldSchema, +} from '@/lib/sdk/catalog-manager/types' + +interface CatalogEntryFormProps { + catalog: CatalogMeta + entry?: CatalogEntry | null + onSave: (data: Record) => void + onCancel: () => void +} + +export default function CatalogEntryForm({ + catalog, + entry, + onSave, + onCancel, +}: CatalogEntryFormProps) { + const isEditMode = entry !== null && entry !== undefined + const isSystemEntry = isEditMode && entry?.source === 'system' + const isCreateMode = !isEditMode + + const title = isSystemEntry + ? 'Details' + : isEditMode + ? 'Eintrag bearbeiten' + : 'Neuer Eintrag' + + const [formData, setFormData] = useState>({}) + const [errors, setErrors] = useState>({}) + + // Initialize form data + useEffect(() => { + if (isEditMode && entry) { + const initialData: Record = {} + for (const field of catalog.fields) { + initialData[field.key] = entry.data?.[field.key] ?? getDefaultValue(field) + } + setFormData(initialData) + } else { + const initialData: Record = {} + for (const field of catalog.fields) { + initialData[field.key] = getDefaultValue(field) + } + setFormData(initialData) + } + }, [entry, catalog.fields, isEditMode]) + + function getDefaultValue(field: CatalogFieldSchema): unknown { + switch (field.type) { + case 'text': + case 'textarea': + return '' + case 'number': + return field.min ?? 0 + case 'select': + return '' + case 'multiselect': + return [] + case 'boolean': + return false + case 'tags': + return '' + default: + return '' + } + } + + const handleFieldChange = useCallback( + (key: string, value: unknown) => { + setFormData(prev => ({ ...prev, [key]: value })) + // Clear error on change + if (errors[key]) { + setErrors(prev => { + const next = { ...prev } + delete next[key] + return next + }) + } + }, + [errors] + ) + + const handleMultiselectToggle = useCallback( + (key: string, option: string) => { + setFormData(prev => { + const current = (prev[key] as string[]) || [] + const updated = current.includes(option) + ? current.filter(v => v !== option) + : [...current, option] + return { ...prev, [key]: updated } + }) + }, + [] + ) + + const validate = (): boolean => { + const newErrors: Record = {} + + for (const field of catalog.fields) { + if (field.required) { + const value = formData[field.key] + if (value === undefined || value === null || value === '') { + newErrors[field.key] = `${field.label} ist ein Pflichtfeld` + } else if (Array.isArray(value) && value.length === 0) { + newErrors[field.key] = `Mindestens ein Wert erforderlich` + } + } + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (isSystemEntry) return + + if (validate()) { + // Convert tags string to array before saving + const processedData = { ...formData } + for (const field of catalog.fields) { + if (field.type === 'tags' && typeof processedData[field.key] === 'string') { + processedData[field.key] = (processedData[field.key] as string) + .split(',') + .map(t => t.trim()) + .filter(Boolean) + } + } + onSave(processedData) + } + } + + // Close on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCancel() + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [onCancel]) + + const renderField = (field: CatalogFieldSchema) => { + const value = formData[field.key] + const hasError = !!errors[field.key] + const isDisabled = isSystemEntry + + const baseInputClasses = `w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent transition-colors ${ + hasError + ? 'border-red-400 dark:border-red-500' + : 'border-gray-300 dark:border-gray-600' + } ${isDisabled ? 'opacity-60 cursor-not-allowed bg-gray-50 dark:bg-gray-800' : ''}` + + switch (field.type) { + case 'text': + return ( + handleFieldChange(field.key, e.target.value)} + placeholder={field.placeholder || ''} + disabled={isDisabled} + className={baseInputClasses} + /> + ) + + case 'textarea': + return ( +