Restore missing document-generator files
Restores index.ts (component exports) and page.tsx (793 lines) for the SDK document generator module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Document Generator Components
|
||||
*
|
||||
* Diese Komponenten integrieren die Einwilligungen-Datenpunkte
|
||||
* in den Dokumentengenerator.
|
||||
*/
|
||||
|
||||
export { DataPointsPreview } from './DataPointsPreview'
|
||||
export { DocumentValidation } from './DocumentValidation'
|
||||
793
admin-v2/app/(sdk)/sdk/document-generator/page.tsx
Normal file
793
admin-v2/app/(sdk)/sdk/document-generator/page.tsx
Normal file
@@ -0,0 +1,793 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import { useEinwilligungen } from '@/lib/sdk/einwilligungen/context'
|
||||
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
||||
import {
|
||||
LegalTemplateResult,
|
||||
TemplateType,
|
||||
Jurisdiction,
|
||||
LicenseType,
|
||||
GeneratedDocument,
|
||||
TEMPLATE_TYPE_LABELS,
|
||||
LICENSE_TYPE_LABELS,
|
||||
JURISDICTION_LABELS,
|
||||
DEFAULT_PLACEHOLDERS,
|
||||
} from '@/lib/sdk/types'
|
||||
import { DataPointsPreview } from './components/DataPointsPreview'
|
||||
import { DocumentValidation } from './components/DocumentValidation'
|
||||
import { generateAllPlaceholders } from '@/lib/sdk/document-generator/datapoint-helpers'
|
||||
|
||||
// =============================================================================
|
||||
// API CLIENT
|
||||
// =============================================================================
|
||||
|
||||
const KLAUSUR_SERVICE_URL = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
|
||||
|
||||
async function searchTemplates(params: {
|
||||
query: string
|
||||
templateType?: TemplateType
|
||||
licenseTypes?: LicenseType[]
|
||||
language?: 'de' | 'en'
|
||||
jurisdiction?: Jurisdiction
|
||||
limit?: number
|
||||
}): Promise<LegalTemplateResult[]> {
|
||||
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: params.query,
|
||||
template_type: params.templateType,
|
||||
license_types: params.licenseTypes,
|
||||
language: params.language,
|
||||
jurisdiction: params.jurisdiction,
|
||||
limit: params.limit || 10,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Search failed')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.map((r: any) => ({
|
||||
id: r.id,
|
||||
score: r.score,
|
||||
text: r.text,
|
||||
documentTitle: r.document_title,
|
||||
templateType: r.template_type,
|
||||
clauseCategory: r.clause_category,
|
||||
language: r.language,
|
||||
jurisdiction: r.jurisdiction,
|
||||
licenseId: r.license_id,
|
||||
licenseName: r.license_name,
|
||||
licenseUrl: r.license_url,
|
||||
attributionRequired: r.attribution_required,
|
||||
attributionText: r.attribution_text,
|
||||
sourceName: r.source_name,
|
||||
sourceUrl: r.source_url,
|
||||
sourceRepo: r.source_repo,
|
||||
placeholders: r.placeholders || [],
|
||||
isCompleteDocument: r.is_complete_document,
|
||||
isModular: r.is_modular,
|
||||
requiresCustomization: r.requires_customization,
|
||||
outputAllowed: r.output_allowed ?? true,
|
||||
modificationAllowed: r.modification_allowed ?? true,
|
||||
distortionProhibited: r.distortion_prohibited ?? false,
|
||||
}))
|
||||
}
|
||||
|
||||
async function getTemplatesStatus(): Promise<any> {
|
||||
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/status`)
|
||||
if (!response.ok) return null
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function getSources(): Promise<any[]> {
|
||||
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/sources`)
|
||||
if (!response.ok) return []
|
||||
const data = await response.json()
|
||||
return data.sources || []
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENTS
|
||||
// =============================================================================
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
ready: 'bg-green-100 text-green-700',
|
||||
empty: 'bg-yellow-100 text-yellow-700',
|
||||
error: 'bg-red-100 text-red-700',
|
||||
running: 'bg-blue-100 text-blue-700',
|
||||
}
|
||||
return (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${colors[status] || 'bg-gray-100 text-gray-700'}`}>
|
||||
{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function LicenseBadge({ licenseId, small = false }: { licenseId: LicenseType | null; small?: boolean }) {
|
||||
if (!licenseId) return null
|
||||
|
||||
const colors: Record<LicenseType, string> = {
|
||||
public_domain: 'bg-green-100 text-green-700 border-green-200',
|
||||
cc0: 'bg-green-100 text-green-700 border-green-200',
|
||||
unlicense: 'bg-green-100 text-green-700 border-green-200',
|
||||
mit: 'bg-blue-100 text-blue-700 border-blue-200',
|
||||
cc_by_4: 'bg-purple-100 text-purple-700 border-purple-200',
|
||||
reuse_notice: 'bg-orange-100 text-orange-700 border-orange-200',
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`${small ? 'px-1.5 py-0.5 text-[10px]' : 'px-2 py-1 text-xs'} rounded border ${colors[licenseId]}`}>
|
||||
{LICENSE_TYPE_LABELS[licenseId] || licenseId}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function TemplateCard({
|
||||
template,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
template: LegalTemplateResult
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={onSelect}
|
||||
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
||||
selected
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-gray-200 hover:border-purple-300 bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-gray-900">
|
||||
{template.documentTitle || 'Untitled'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded">
|
||||
{template.score.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
{template.templateType && (
|
||||
<span className="text-xs text-purple-600 bg-purple-100 px-2 py-0.5 rounded">
|
||||
{TEMPLATE_TYPE_LABELS[template.templateType as TemplateType] || template.templateType}
|
||||
</span>
|
||||
)}
|
||||
<LicenseBadge licenseId={template.licenseId as LicenseType} small />
|
||||
<span className="text-xs text-gray-500 uppercase">
|
||||
{template.language}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={onSelect}
|
||||
className="w-5 h-5 text-purple-600 rounded border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 line-clamp-3 mt-2">
|
||||
{template.text}
|
||||
</p>
|
||||
|
||||
{template.attributionRequired && template.attributionText && (
|
||||
<div className="mt-2 text-xs text-orange-600 bg-orange-50 p-2 rounded">
|
||||
Attribution: {template.attributionText}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{template.placeholders && template.placeholders.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{template.placeholders.slice(0, 5).map((p, i) => (
|
||||
<span key={i} className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">
|
||||
{p}
|
||||
</span>
|
||||
))}
|
||||
{template.placeholders.length > 5 && (
|
||||
<span className="text-xs text-gray-500">
|
||||
+{template.placeholders.length - 5} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-xs text-gray-500">
|
||||
Source: {template.sourceName}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PlaceholderEditor({
|
||||
placeholders,
|
||||
values,
|
||||
onChange,
|
||||
}: {
|
||||
placeholders: string[]
|
||||
values: Record<string, string>
|
||||
onChange: (key: string, value: string) => void
|
||||
}) {
|
||||
if (placeholders.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="bg-blue-50 rounded-xl p-4 border border-blue-200">
|
||||
<h4 className="font-medium text-blue-900 mb-3">Platzhalter ausfuellen</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{placeholders.map((placeholder) => (
|
||||
<div key={placeholder}>
|
||||
<label className="block text-sm text-blue-700 mb-1">{placeholder}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={values[placeholder] || ''}
|
||||
onChange={(e) => onChange(placeholder, e.target.value)}
|
||||
placeholder={`Wert fuer ${placeholder}`}
|
||||
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AttributionFooter({ templates }: { templates: LegalTemplateResult[] }) {
|
||||
const attributionTemplates = templates.filter((t) => t.attributionRequired)
|
||||
if (attributionTemplates.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-xl p-4 border border-gray-200">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Quellenangaben (werden automatisch hinzugefuegt)</h4>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<p>Dieses Dokument wurde unter Verwendung folgender Quellen erstellt:</p>
|
||||
<ul className="list-disc list-inside ml-2">
|
||||
{attributionTemplates.map((t, i) => (
|
||||
<li key={i}>
|
||||
{t.attributionText || `${t.sourceName} (${t.licenseName})`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DocumentPreview({
|
||||
content,
|
||||
placeholders,
|
||||
}: {
|
||||
content: string
|
||||
placeholders: Record<string, string>
|
||||
}) {
|
||||
// Replace placeholders in content
|
||||
let processedContent = content
|
||||
for (const [key, value] of Object.entries(placeholders)) {
|
||||
if (value) {
|
||||
processedContent = processedContent.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 prose prose-sm max-w-none">
|
||||
<div className="whitespace-pre-wrap">{processedContent}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN PAGE
|
||||
// =============================================================================
|
||||
|
||||
export default function DocumentGeneratorPage() {
|
||||
const { state } = useSDK()
|
||||
const { selectedDataPointsData } = useEinwilligungen()
|
||||
|
||||
// Status state
|
||||
const [status, setStatus] = useState<any>(null)
|
||||
const [sources, setSources] = useState<any[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
// Search state
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedType, setSelectedType] = useState<TemplateType | ''>('')
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<'de' | 'en' | ''>('')
|
||||
const [selectedJurisdiction, setSelectedJurisdiction] = useState<Jurisdiction | ''>('')
|
||||
const [searchResults, setSearchResults] = useState<LegalTemplateResult[]>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
|
||||
// Selection state
|
||||
const [selectedTemplates, setSelectedTemplates] = useState<string[]>([])
|
||||
|
||||
// Editor state
|
||||
const [placeholderValues, setPlaceholderValues] = useState<Record<string, string>>({})
|
||||
const [activeTab, setActiveTab] = useState<'search' | 'compose' | 'preview'>('search')
|
||||
|
||||
// Load initial status
|
||||
useEffect(() => {
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const [statusData, sourcesData] = await Promise.all([
|
||||
getTemplatesStatus(),
|
||||
getSources(),
|
||||
])
|
||||
setStatus(statusData)
|
||||
setSources(sourcesData)
|
||||
} catch (error) {
|
||||
console.error('Failed to load status:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
loadStatus()
|
||||
}, [])
|
||||
|
||||
// Pre-fill placeholders from company profile
|
||||
useEffect(() => {
|
||||
if (state?.companyProfile) {
|
||||
const profile = state.companyProfile
|
||||
setPlaceholderValues((prev) => ({
|
||||
...prev,
|
||||
'[COMPANY_NAME]': profile.companyName || '',
|
||||
'[FIRMENNAME]': profile.companyName || '',
|
||||
'[EMAIL]': profile.dpoEmail || '',
|
||||
'[DSB_EMAIL]': profile.dpoEmail || '',
|
||||
'[DPO_NAME]': profile.dpoName || '',
|
||||
'[DSB_NAME]': profile.dpoName || '',
|
||||
}))
|
||||
}
|
||||
}, [state?.companyProfile])
|
||||
|
||||
// Pre-fill placeholders from Einwilligungen data points
|
||||
useEffect(() => {
|
||||
if (selectedDataPointsData && selectedDataPointsData.length > 0) {
|
||||
const einwilligungenPlaceholders = generateAllPlaceholders(selectedDataPointsData, 'de')
|
||||
setPlaceholderValues((prev) => ({
|
||||
...prev,
|
||||
...einwilligungenPlaceholders,
|
||||
}))
|
||||
}
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
// Handler for inserting placeholders from DataPointsPreview
|
||||
const handleInsertPlaceholder = useCallback((placeholder: string) => {
|
||||
// This is a simplified version - in a real editor you would insert at cursor position
|
||||
// For now, we just ensure the placeholder is in the values so it can be replaced
|
||||
if (!placeholderValues[placeholder]) {
|
||||
// The placeholder value will be generated from einwilligungen data
|
||||
const einwilligungenPlaceholders = generateAllPlaceholders(selectedDataPointsData || [], 'de')
|
||||
if (einwilligungenPlaceholders[placeholder as keyof typeof einwilligungenPlaceholders]) {
|
||||
setPlaceholderValues((prev) => ({
|
||||
...prev,
|
||||
[placeholder]: einwilligungenPlaceholders[placeholder as keyof typeof einwilligungenPlaceholders],
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, [placeholderValues, selectedDataPointsData])
|
||||
|
||||
// Search handler
|
||||
const handleSearch = useCallback(async () => {
|
||||
if (!searchQuery.trim()) return
|
||||
|
||||
setIsSearching(true)
|
||||
try {
|
||||
const results = await searchTemplates({
|
||||
query: searchQuery,
|
||||
templateType: selectedType || undefined,
|
||||
language: selectedLanguage || undefined,
|
||||
jurisdiction: selectedJurisdiction || undefined,
|
||||
limit: 20,
|
||||
})
|
||||
setSearchResults(results)
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
}, [searchQuery, selectedType, selectedLanguage, selectedJurisdiction])
|
||||
|
||||
// Toggle template selection
|
||||
const toggleTemplate = (id: string) => {
|
||||
setSelectedTemplates((prev) =>
|
||||
prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]
|
||||
)
|
||||
}
|
||||
|
||||
// Get selected template objects
|
||||
const selectedTemplateObjects = searchResults.filter((r) =>
|
||||
selectedTemplates.includes(r.id)
|
||||
)
|
||||
|
||||
// Get all unique placeholders from selected templates
|
||||
const allPlaceholders = Array.from(
|
||||
new Set(selectedTemplateObjects.flatMap((t) => t.placeholders || []))
|
||||
)
|
||||
|
||||
// Combined content from selected templates
|
||||
const combinedContent = selectedTemplateObjects
|
||||
.map((t) => `## ${t.documentTitle || 'Abschnitt'}\n\n${t.text}`)
|
||||
.join('\n\n---\n\n')
|
||||
|
||||
// Step info - using 'consent' as base since document-generator doesn't exist yet
|
||||
const stepInfo = STEP_EXPLANATIONS['consent'] || {
|
||||
title: 'Dokumentengenerator',
|
||||
description: 'Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen',
|
||||
explanation: 'Der Dokumentengenerator nutzt frei lizenzierte Textbausteine um Datenschutzerklaerungen, AGB und andere rechtliche Dokumente zu erstellen.',
|
||||
tips: ['Waehlen Sie passende Vorlagen aus der Suche', 'Fuellen Sie die Platzhalter mit Ihren Unternehmensdaten'],
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Step Header */}
|
||||
<StepHeader
|
||||
stepId="document-generator"
|
||||
title="Dokumentengenerator"
|
||||
description="Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen"
|
||||
explanation={stepInfo.explanation}
|
||||
tips={stepInfo.tips}
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveTab('compose')}
|
||||
disabled={selectedTemplates.length === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Dokument erstellen ({selectedTemplates.length})
|
||||
</button>
|
||||
</StepHeader>
|
||||
|
||||
{/* Status Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="text-sm text-gray-500">Collection Status</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<StatusBadge status={status?.stats?.status || 'unknown'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="text-sm text-gray-500">Indexierte Chunks</div>
|
||||
<div className="text-3xl font-bold text-gray-900">
|
||||
{status?.stats?.points_count || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="text-sm text-gray-500">Aktive Quellen</div>
|
||||
<div className="text-3xl font-bold text-purple-600">
|
||||
{sources.filter((s) => s.enabled).length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="text-sm text-gray-500">Ausgewaehlt</div>
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{selectedTemplates.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{(['search', 'compose', 'preview'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
disabled={tab !== 'search' && selectedTemplates.length === 0}
|
||||
className={`px-4 py-2 font-medium transition-colors ${
|
||||
activeTab === tab
|
||||
? 'text-purple-600 border-b-2 border-purple-600'
|
||||
: 'text-gray-500 hover:text-gray-700 disabled:text-gray-300 disabled:cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{tab === 'search' && 'Vorlagen suchen'}
|
||||
{tab === 'compose' && 'Zusammenstellen'}
|
||||
{tab === 'preview' && 'Vorschau'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search Tab */}
|
||||
{activeTab === 'search' && (
|
||||
<div className="space-y-4">
|
||||
{/* Search Form */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex gap-4 items-end flex-wrap">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Suche
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="z.B. Datenschutzerklaerung, Cookie-Banner, Widerruf..."
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Dokumenttyp
|
||||
</label>
|
||||
<select
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value as TemplateType | '')}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
{Object.entries(TEMPLATE_TYPE_LABELS).map(([key, label]) => (
|
||||
<option key={key} value={key}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sprache
|
||||
</label>
|
||||
<select
|
||||
value={selectedLanguage}
|
||||
onChange={(e) => setSelectedLanguage(e.target.value as 'de' | 'en' | '')}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Alle</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={isSearching || !searchQuery.trim()}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isSearching ? 'Suche...' : 'Suchen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Results */}
|
||||
{searchResults.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-gray-900">
|
||||
{searchResults.length} Ergebnisse
|
||||
</h3>
|
||||
{selectedTemplates.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTemplates([])}
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Auswahl aufheben
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{searchResults.map((result) => (
|
||||
<TemplateCard
|
||||
key={result.id}
|
||||
template={result}
|
||||
selected={selectedTemplates.includes(result.id)}
|
||||
onSelect={() => toggleTemplate(result.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchResults.length === 0 && searchQuery && !isSearching && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||||
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="w-8 h-8 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>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Keine Vorlagen gefunden</h3>
|
||||
<p className="mt-2 text-gray-500">
|
||||
Versuchen Sie einen anderen Suchbegriff oder aendern Sie die Filter.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Start Templates */}
|
||||
{searchResults.length === 0 && !searchQuery && (
|
||||
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-xl border border-purple-200 p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Schnellstart - Haeufig benoetigte Dokumente</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ query: 'Datenschutzerklaerung DSGVO', type: 'privacy_policy', icon: '🔒' },
|
||||
{ query: 'Cookie Banner', type: 'cookie_banner', icon: '🍪' },
|
||||
{ query: 'Impressum', type: 'impressum', icon: '📋' },
|
||||
{ query: 'AGB Nutzungsbedingungen', type: 'terms_of_service', icon: '📜' },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.type}
|
||||
onClick={() => {
|
||||
setSearchQuery(item.query)
|
||||
setSelectedType(item.type as TemplateType)
|
||||
setTimeout(handleSearch, 100)
|
||||
}}
|
||||
className="p-4 bg-white rounded-lg border border-gray-200 hover:border-purple-300 hover:shadow transition-all text-center"
|
||||
>
|
||||
<span className="text-3xl mb-2 block">{item.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{TEMPLATE_TYPE_LABELS[item.type as TemplateType]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Compose Tab */}
|
||||
{activeTab === 'compose' && selectedTemplates.length > 0 && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Main Content - 2/3 */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Selected Templates */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">
|
||||
Ausgewaehlte Bausteine ({selectedTemplates.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{selectedTemplateObjects.map((t, index) => (
|
||||
<div
|
||||
key={t.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-gray-400 font-mono">{index + 1}.</span>
|
||||
<span className="font-medium">{t.documentTitle}</span>
|
||||
<LicenseBadge licenseId={t.licenseId as LicenseType} small />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => toggleTemplate(t.id)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Placeholder Editor */}
|
||||
<PlaceholderEditor
|
||||
placeholders={allPlaceholders}
|
||||
values={placeholderValues}
|
||||
onChange={(key, value) =>
|
||||
setPlaceholderValues((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Attribution Footer */}
|
||||
<AttributionFooter templates={selectedTemplateObjects} />
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('search')}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
Zurueck zur Suche
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('preview')}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Vorschau anzeigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar - 1/3: Einwilligungen DataPoints */}
|
||||
<div className="lg:col-span-1">
|
||||
<DataPointsPreview
|
||||
dataPoints={selectedDataPointsData || []}
|
||||
onInsertPlaceholder={handleInsertPlaceholder}
|
||||
language="de"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview Tab */}
|
||||
{activeTab === 'preview' && selectedTemplates.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-gray-900">Dokument-Vorschau</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('compose')}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Copy to clipboard
|
||||
let content = combinedContent
|
||||
for (const [key, value] of Object.entries(placeholderValues)) {
|
||||
if (value) {
|
||||
content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value)
|
||||
}
|
||||
}
|
||||
navigator.clipboard.writeText(content)
|
||||
}}
|
||||
className="px-4 py-2 text-purple-700 bg-purple-100 rounded-lg hover:bg-purple-200 transition-colors"
|
||||
>
|
||||
Kopieren
|
||||
</button>
|
||||
<button
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Als PDF exportieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document Validation based on selected Einwilligungen */}
|
||||
{selectedDataPointsData && selectedDataPointsData.length > 0 && (
|
||||
<DocumentValidation
|
||||
dataPoints={selectedDataPointsData}
|
||||
documentContent={combinedContent}
|
||||
language="de"
|
||||
onInsertPlaceholder={handleInsertPlaceholder}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DocumentPreview
|
||||
content={combinedContent}
|
||||
placeholders={placeholderValues}
|
||||
/>
|
||||
|
||||
{/* Attribution */}
|
||||
<AttributionFooter templates={selectedTemplateObjects} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sources Info */}
|
||||
{activeTab === 'search' && sources.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Verfuegbare Quellen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{sources.filter((s) => s.enabled).slice(0, 6).map((source) => (
|
||||
<div key={source.name} className="p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-medium text-gray-900">{source.name}</span>
|
||||
<LicenseBadge licenseId={source.license_type as LicenseType} small />
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 line-clamp-2">{source.description}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{source.template_types.slice(0, 3).map((t: string) => (
|
||||
<span key={t} className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">
|
||||
{TEMPLATE_TYPE_LABELS[t as TemplateType] || t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user