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:
292
admin-v2/app/(admin)/education/edu-search/page.tsx
Normal file
292
admin-v2/app/(admin)/education/edu-search/page.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Education Search Page
|
||||
* Bildungsquellen und Crawler-Verwaltung
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
import { Search, Database, RefreshCw, ExternalLink, FileText, BookOpen } from 'lucide-react'
|
||||
|
||||
interface DataSource {
|
||||
id: string
|
||||
name: string
|
||||
type: 'api' | 'crawler' | 'manual'
|
||||
status: 'active' | 'inactive' | 'error'
|
||||
lastUpdate?: string
|
||||
documentCount: number
|
||||
url?: string
|
||||
}
|
||||
|
||||
const DATA_SOURCES: DataSource[] = [
|
||||
{
|
||||
id: 'nibis',
|
||||
name: 'NiBiS (Niedersachsen)',
|
||||
type: 'crawler',
|
||||
status: 'active',
|
||||
lastUpdate: '2026-01-20',
|
||||
documentCount: 1250,
|
||||
url: 'https://nibis.de',
|
||||
},
|
||||
{
|
||||
id: 'kmk',
|
||||
name: 'KMK Beschluesse',
|
||||
type: 'crawler',
|
||||
status: 'active',
|
||||
lastUpdate: '2026-01-10',
|
||||
documentCount: 450,
|
||||
url: 'https://kmk.org',
|
||||
},
|
||||
]
|
||||
|
||||
export default function EduSearchPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [activeTab, setActiveTab] = useState<'search' | 'sources' | 'crawler'>('search')
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PagePurpose
|
||||
title="Education Search"
|
||||
purpose="Durchsuchen Sie Bildungsquellen und verwalten Sie Crawler fuer Lehrplaene, Erlasse und Schulinformationen. Zentraler Zugang zu bildungsrelevanten Dokumenten."
|
||||
audience={['Content Manager', 'Entwickler', 'Bildungs-Admins']}
|
||||
architecture={{
|
||||
services: ['edu-search-service (Go)', 'OpenSearch'],
|
||||
databases: ['OpenSearch (bp_documents_v1)', 'PostgreSQL'],
|
||||
}}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{DATA_SOURCES.reduce((sum, s) => sum + s.documentCount, 0).toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Dokumente gesamt</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-3xl font-bold text-green-600">{DATA_SOURCES.length}</div>
|
||||
<div className="text-sm text-slate-500">Datenquellen</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-3xl font-bold text-purple-600">
|
||||
{DATA_SOURCES.filter(s => s.type === 'crawler').length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Aktive Crawler</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-3xl font-bold text-orange-600">16</div>
|
||||
<div className="text-sm text-slate-500">Bundeslaender</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('search')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeTab === 'search'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Search className="w-4 h-4 inline mr-2" />
|
||||
Suche
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('sources')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeTab === 'sources'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Database className="w-4 h-4 inline mr-2" />
|
||||
Datenquellen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('crawler')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeTab === 'crawler'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 inline mr-2" />
|
||||
Crawler
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search Tab */}
|
||||
{activeTab === 'search' && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<div className="flex gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Suche nach Lehrplaenen, Erlassen, Curricula..."
|
||||
className="flex-1 px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
|
||||
/>
|
||||
<button className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||
Suchen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
<span className="text-sm text-slate-500 mr-2">Schnellfilter:</span>
|
||||
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
|
||||
Lehrplaene
|
||||
</button>
|
||||
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
|
||||
Erlasse
|
||||
</button>
|
||||
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
|
||||
Kerncurricula
|
||||
</button>
|
||||
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
|
||||
Abitur
|
||||
</button>
|
||||
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
|
||||
Niedersachsen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<BookOpen className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>Geben Sie einen Suchbegriff ein, um Bildungsdokumente zu durchsuchen</p>
|
||||
<p className="text-sm mt-2">Die Suche durchsucht alle angebundenen Datenquellen</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sources Tab */}
|
||||
{activeTab === 'sources' && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-medium text-slate-600">Datenquelle</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-slate-600">Typ</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-slate-600">Status</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-slate-600">Dokumente</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-slate-600">Letztes Update</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-slate-600">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{DATA_SOURCES.map((source) => (
|
||||
<tr key={source.id} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="w-4 h-4 text-slate-400" />
|
||||
<div className="font-medium text-slate-900">{source.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
||||
source.type === 'api' ? 'bg-blue-100 text-blue-700' :
|
||||
source.type === 'crawler' ? 'bg-purple-100 text-purple-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{source.type.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
||||
source.status === 'active' ? 'bg-green-100 text-green-700' :
|
||||
source.status === 'error' ? 'bg-red-100 text-red-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{source.status === 'active' ? 'Aktiv' : source.status === 'error' ? 'Fehler' : 'Inaktiv'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-medium">
|
||||
{source.documentCount.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-500">
|
||||
{source.lastUpdate || '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{source.url && (
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded inline-block"
|
||||
title="Quelle oeffnen"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Crawler Tab */}
|
||||
{activeTab === 'crawler' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Crawler-Verwaltung</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Hier koennen Sie die Crawler fuer verschiedene Bildungsquellen steuern.
|
||||
Das System crawlt ausschliesslich oeffentliche Bildungsdokumente (Lehrplaene, Erlasse, Curricula). Keine Personendaten.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className="w-5 h-5 text-purple-600" />
|
||||
<span className="font-medium">NiBiS Crawler</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-3">
|
||||
Crawlt Lehrplaene und Erlasse aus Niedersachsen
|
||||
</p>
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">
|
||||
Crawl starten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className="w-5 h-5 text-blue-600" />
|
||||
<span className="font-medium">KMK Crawler</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-3">
|
||||
Crawlt Beschluesse der Kultusministerkonferenz
|
||||
</p>
|
||||
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">
|
||||
Crawl starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
<span>ℹ️</span>
|
||||
Verwandte Module
|
||||
</h3>
|
||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<a href="/education/zeugnisse-crawler" className="block p-3 bg-white rounded-lg hover:shadow-md transition-shadow">
|
||||
<div className="font-medium text-slate-900">Zeugnisse-Crawler</div>
|
||||
<div className="text-sm text-slate-500">Zeugnis-Strukturen verwalten</div>
|
||||
</a>
|
||||
<a href="/education/training" className="block p-3 bg-white rounded-lg hover:shadow-md transition-shadow">
|
||||
<div className="font-medium text-slate-900">Training</div>
|
||||
<div className="text-sm text-slate-500">Schulungsmodule verwalten</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
// Types
|
||||
interface WizardStep {
|
||||
number: number
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
description: string
|
||||
icon: string
|
||||
is_required: boolean
|
||||
is_completed: boolean
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8080'
|
||||
|
||||
// Step icons mapping
|
||||
const stepIcons: Record<string, React.ReactNode> = {
|
||||
'document-text': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
),
|
||||
'academic-cap': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l9-5-9-5-9 5 9 5z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
|
||||
</svg>
|
||||
),
|
||||
'server': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
|
||||
</svg>
|
||||
),
|
||||
'document-report': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
),
|
||||
'currency-euro': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.121 15.536c-1.171 1.952-3.07 1.952-4.242 0-1.172-1.953-1.172-5.119 0-7.072 1.171-1.952 3.07-1.952 4.242 0M8 10.5h4m-4 3h4m9-1.5a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
'calculator': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
'calendar': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
'document-download': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
// Default wizard steps
|
||||
const defaultSteps: WizardStep[] = [
|
||||
{ number: 1, id: 'foerderprogramm', title: 'Foerderprogramm', subtitle: 'Programm & Grunddaten', description: 'Waehlen Sie das Foerderprogramm', icon: 'document-text', is_required: true, is_completed: false },
|
||||
{ number: 2, id: 'schulinformationen', title: 'Schulinformationen', subtitle: 'Schule & Traeger', description: 'Angaben zur Schule', icon: 'academic-cap', is_required: true, is_completed: false },
|
||||
{ number: 3, id: 'bestandsaufnahme', title: 'IT-Bestand', subtitle: 'Aktuelle Infrastruktur', description: 'IT-Bestandsaufnahme', icon: 'server', is_required: true, is_completed: false },
|
||||
{ number: 4, id: 'projektbeschreibung', title: 'Projektbeschreibung', subtitle: 'Ziele & Didaktik', description: 'Projektziele beschreiben', icon: 'document-report', is_required: true, is_completed: false },
|
||||
{ number: 5, id: 'investitionen', title: 'Investitionen', subtitle: 'Kostenaufstellung', description: 'Geplante Anschaffungen', icon: 'currency-euro', is_required: true, is_completed: false },
|
||||
{ number: 6, id: 'finanzierungsplan', title: 'Finanzierung', subtitle: 'Budget & Eigenanteil', description: 'Finanzierungsplan', icon: 'calculator', is_required: true, is_completed: false },
|
||||
{ number: 7, id: 'zeitplan', title: 'Zeitplan', subtitle: 'Laufzeit & Meilensteine', description: 'Projektlaufzeit planen', icon: 'calendar', is_required: true, is_completed: false },
|
||||
{ number: 8, id: 'abschluss', title: 'Abschluss', subtitle: 'Dokumente & Pruefung', description: 'Zusammenfassung', icon: 'document-download', is_required: true, is_completed: false },
|
||||
]
|
||||
|
||||
export default function FoerderantragWizardPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const applicationId = params.applicationId as string
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [steps, setSteps] = useState<WizardStep[]>(defaultSteps)
|
||||
const [formData, setFormData] = useState<FormData>({})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [showAssistant, setShowAssistant] = useState(false)
|
||||
const [assistantMessage, setAssistantMessage] = useState('')
|
||||
const [assistantHistory, setAssistantHistory] = useState<{ role: string; content: string }[]>([])
|
||||
const [isDemo, setIsDemo] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check if this is a demo application
|
||||
if (applicationId.startsWith('demo-')) {
|
||||
setIsDemo(true)
|
||||
}
|
||||
loadApplication()
|
||||
}, [applicationId])
|
||||
|
||||
const loadApplication = async () => {
|
||||
// In production, load from API
|
||||
// For demo, use mock data
|
||||
}
|
||||
|
||||
const handleFieldChange = (fieldId: string, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[`step_${currentStep}`]: {
|
||||
...prev[`step_${currentStep}`],
|
||||
[fieldId]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSaveStep = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
// Save step data
|
||||
// Update step completion status
|
||||
setSteps(prev => prev.map(s =>
|
||||
s.number === currentStep ? { ...s, is_completed: true } : s
|
||||
))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNextStep = async () => {
|
||||
await handleSaveStep()
|
||||
if (currentStep < 8) {
|
||||
setCurrentStep(prev => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevStep = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(prev => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAskAssistant = async () => {
|
||||
if (!assistantMessage.trim()) return
|
||||
|
||||
const userMessage = assistantMessage
|
||||
setAssistantMessage('')
|
||||
setAssistantHistory(prev => [...prev, { role: 'user', content: userMessage }])
|
||||
|
||||
// Simulate assistant response
|
||||
setTimeout(() => {
|
||||
const response = getAssistantResponse(userMessage, currentStep)
|
||||
setAssistantHistory(prev => [...prev, { role: 'assistant', content: response }])
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const getAssistantResponse = (question: string, step: number): string => {
|
||||
// Simple response logic - in production, this calls the LLM API
|
||||
if (question.toLowerCase().includes('foerderquote')) {
|
||||
return 'Die Foerderquote im DigitalPakt 2.0 betraegt in der Regel 90%. Das bedeutet, dass 10% der Kosten als Eigenanteil vom Schultraeger zu tragen sind. In einigen Bundeslaendern gibt es Sonderregelungen fuer finanzschwache Kommunen.'
|
||||
}
|
||||
if (question.toLowerCase().includes('mep') || question.toLowerCase().includes('medienentwicklungsplan')) {
|
||||
return 'Der Medienentwicklungsplan (MEP) ist ein strategisches Dokument, das die paedagogischen und technischen Ziele der Schule fuer die Digitalisierung beschreibt. In den meisten Bundeslaendern ist ein MEP Voraussetzung fuer die Foerderung.'
|
||||
}
|
||||
if (question.toLowerCase().includes('foerderfahig')) {
|
||||
return 'Foerderfahig sind unter anderem: Netzwerkinfrastruktur, WLAN, Praesentationstechnik, Endgeraete (mit Einschraenkungen), Server und lokale KI-Systeme. Nicht foerderfahig sind: Verbrauchsmaterial, laufende Betriebskosten und Cloud-Abonnements ohne lokale Alternative.'
|
||||
}
|
||||
return `Ich helfe Ihnen gerne bei Schritt ${step}. Haben Sie eine konkrete Frage zu den Feldern in diesem Abschnitt? Sie koennen mich auch nach Formulierungshilfen oder Erklaerungen zu Fachbegriffen fragen.`
|
||||
}
|
||||
|
||||
const renderStepContent = () => {
|
||||
const step = steps.find(s => s.number === currentStep)
|
||||
if (!step) return null
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return <Step1Foerderprogramm formData={formData} onChange={handleFieldChange} />
|
||||
case 2:
|
||||
return <Step2Schulinformationen formData={formData} onChange={handleFieldChange} />
|
||||
case 3:
|
||||
return <Step3Bestandsaufnahme formData={formData} onChange={handleFieldChange} />
|
||||
case 4:
|
||||
return <Step4Projektbeschreibung formData={formData} onChange={handleFieldChange} />
|
||||
case 5:
|
||||
return <Step5Investitionen formData={formData} onChange={handleFieldChange} />
|
||||
case 6:
|
||||
return <Step6Finanzierungsplan formData={formData} onChange={handleFieldChange} />
|
||||
case 7:
|
||||
return <Step7Zeitplan formData={formData} onChange={handleFieldChange} />
|
||||
case 8:
|
||||
return <Step8Abschluss formData={formData} onChange={handleFieldChange} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-slate-200 sticky top-0 z-20">
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/education/foerderantrag"
|
||||
className="p-2 rounded-lg hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="font-semibold text-slate-900">Foerderantrag bearbeiten</h1>
|
||||
<p className="text-sm text-slate-500">
|
||||
Schritt {currentStep} von {steps.length}: {steps.find(s => s.number === currentStep)?.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isDemo && (
|
||||
<span className="px-3 py-1 bg-amber-100 text-amber-700 text-sm font-medium rounded-full">
|
||||
Demo-Modus
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowAssistant(!showAssistant)}
|
||||
className={`p-2 rounded-lg transition-colors ${showAssistant ? 'bg-blue-100 text-blue-600' : 'hover:bg-slate-100 text-slate-600'}`}
|
||||
title="KI-Assistent"
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveStep}
|
||||
disabled={isSaving}
|
||||
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg font-medium hover:bg-slate-200 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isSaving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="px-6 pb-4 overflow-x-auto">
|
||||
<div className="flex gap-1 min-w-max">
|
||||
{steps.map((step) => (
|
||||
<button
|
||||
key={step.number}
|
||||
onClick={() => setCurrentStep(step.number)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all ${
|
||||
currentStep === step.number
|
||||
? 'bg-blue-600 text-white'
|
||||
: step.is_completed
|
||||
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
|
||||
currentStep === step.number
|
||||
? 'bg-white/20'
|
||||
: step.is_completed
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-slate-300 text-slate-600'
|
||||
}`}>
|
||||
{step.is_completed && currentStep !== step.number ? (
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
step.number
|
||||
)}
|
||||
</span>
|
||||
<span className="hidden md:block font-medium">{step.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex">
|
||||
{/* Form Area */}
|
||||
<div className={`flex-1 p-6 transition-all ${showAssistant ? 'pr-96' : ''}`}>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Step Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center">
|
||||
{stepIcons[steps.find(s => s.number === currentStep)?.icon || 'document-text']}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">
|
||||
{steps.find(s => s.number === currentStep)?.title}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500">
|
||||
{steps.find(s => s.number === currentStep)?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
{renderStepContent()}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<button
|
||||
onClick={handlePrevStep}
|
||||
disabled={currentStep === 1}
|
||||
className="px-6 py-3 text-slate-600 hover:text-slate-900 disabled:opacity-50 disabled:cursor-not-allowed font-medium flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Zurueck
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNextStep}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-xl font-semibold hover:bg-blue-700 flex items-center gap-2 transition-colors"
|
||||
>
|
||||
{currentStep === 8 ? 'Abschliessen' : 'Weiter'}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assistant Sidebar */}
|
||||
{showAssistant && (
|
||||
<div className="fixed right-0 top-0 h-full w-96 bg-white border-l border-slate-200 shadow-xl z-30 flex flex-col">
|
||||
<div className="p-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900">KI-Assistent</h3>
|
||||
<p className="text-xs text-slate-500">Ich helfe bei Fragen</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAssistant(false)}
|
||||
className="p-2 rounded-lg hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 text-slate-500" 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>
|
||||
|
||||
{/* Chat History */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{assistantHistory.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-slate-500 text-sm">
|
||||
Stellen Sie mir Fragen zum aktuellen Schritt oder bitten Sie um Formulierungshilfen.
|
||||
</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
{['Was ist foerderfahig?', 'Erklaere die Foerderquote', 'Was ist ein MEP?'].map((q) => (
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => {
|
||||
setAssistantMessage(q)
|
||||
setTimeout(handleAskAssistant, 100)
|
||||
}}
|
||||
className="block w-full text-left px-3 py-2 bg-slate-100 rounded-lg text-sm text-slate-700 hover:bg-slate-200 transition-colors"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{assistantHistory.map((msg, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] p-3 rounded-xl text-sm ${
|
||||
msg.role === 'user'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t border-slate-200">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={assistantMessage}
|
||||
onChange={(e) => setAssistantMessage(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAskAssistant()}
|
||||
placeholder="Frage stellen..."
|
||||
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAskAssistant}
|
||||
className="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Step Components (simplified for now)
|
||||
function Step1Foerderprogramm({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-slate-600">
|
||||
Die Grunddaten wurden bereits beim Erstellen des Antrags festgelegt.
|
||||
Sie koennen diese hier bei Bedarf anpassen.
|
||||
</p>
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-700">
|
||||
Klicken Sie auf "Weiter" um mit den Schulinformationen fortzufahren.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step2Schulinformationen({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Schulname *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. Gymnasium am Beispielweg"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Schulnummer *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. 12345"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Anzahl Schueler</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. 850"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Anzahl Lehrkraefte</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. 65"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step3Bestandsaufnahme({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<input type="checkbox" id="has_wlan" className="w-4 h-4 rounded border-slate-300" />
|
||||
<label htmlFor="has_wlan" className="text-sm text-slate-700">WLAN vorhanden</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Internet-Bandbreite</label>
|
||||
<select className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option>Unter 16 Mbit/s</option>
|
||||
<option>16-50 Mbit/s</option>
|
||||
<option>50-100 Mbit/s</option>
|
||||
<option>100-250 Mbit/s</option>
|
||||
<option>Ueber 250 Mbit/s</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Vorhandene Endgeraete</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Anzahl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step4Projektbeschreibung({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Kurzbeschreibung *</label>
|
||||
<textarea
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
placeholder="Beschreiben Sie Ihr Projekt in 2-3 Saetzen..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Projektziele *</label>
|
||||
<textarea
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="Welche konkreten Ziele verfolgen Sie?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Paedagogisches Konzept *</label>
|
||||
<textarea
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="Wie wird die Technik im Unterricht eingesetzt?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step5Investitionen({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-slate-600">
|
||||
Listen Sie alle geplanten Investitionen auf. Der Wizard berechnet automatisch die Summen.
|
||||
</p>
|
||||
<div className="border border-slate-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left font-medium text-slate-700">Beschreibung</th>
|
||||
<th className="px-4 py-2 text-left font-medium text-slate-700">Anzahl</th>
|
||||
<th className="px-4 py-2 text-left font-medium text-slate-700">Einzelpreis</th>
|
||||
<th className="px-4 py-2 text-left font-medium text-slate-700">Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-t border-slate-200">
|
||||
<td className="px-4 py-2" colSpan={4}>
|
||||
<button className="text-blue-600 hover:text-blue-700 font-medium text-sm flex items-center gap-1">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Position hinzufuegen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step6Finanzierungsplan({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Foerderquote</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="100"
|
||||
defaultValue="90"
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-lg font-semibold text-slate-900">90%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="text-sm text-slate-500">Gesamtkosten</div>
|
||||
<div className="text-xl font-bold text-slate-900">0,00 EUR</div>
|
||||
</div>
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<div className="text-sm text-blue-600">Foerderbetrag</div>
|
||||
<div className="text-xl font-bold text-blue-700">0,00 EUR</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step7Zeitplan({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Projektbeginn *</label>
|
||||
<input
|
||||
type="date"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Projektende *</label>
|
||||
<input
|
||||
type="date"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Meilensteine</label>
|
||||
<p className="text-sm text-slate-500">Definieren Sie wichtige Projektmeilensteine</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step8Abschluss({ formData, onChange }: { formData: FormData; onChange: (id: string, value: any) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<h3 className="font-semibold text-green-800">Zusammenfassung</h3>
|
||||
<p className="text-sm text-green-700 mt-1">
|
||||
Pruefen Sie alle Angaben und laden Sie ggf. zusaetzliche Dokumente hoch.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Datenschutzkonzept *</label>
|
||||
<textarea
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={4}
|
||||
placeholder="Beschreiben Sie die Massnahmen zum Datenschutz..."
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<h3 className="font-semibold text-amber-800">Hinweis zur Traegerpruefung</h3>
|
||||
<p className="text-sm text-amber-700 mt-1">
|
||||
Der generierte Antrag ist ein antragsfaehiger ENTWURF.
|
||||
Die finale Pruefung und Einreichung erfolgt durch den Schultraeger.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-slate-300" />
|
||||
<span className="text-sm text-slate-700">Ich bestaetige, dass alle Angaben nach bestem Wissen gemacht wurden</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-3">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-slate-300" />
|
||||
<span className="text-sm text-slate-700">Ich habe verstanden, dass der Antrag vom Schultraeger geprueft werden muss</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
368
admin-v2/app/(admin)/education/foerderantrag/new/page.tsx
Normal file
368
admin-v2/app/(admin)/education/foerderantrag/new/page.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
// Types
|
||||
type FundingProgram = 'DIGITALPAKT_1' | 'DIGITALPAKT_2' | 'LANDESFOERDERUNG' | 'SCHULTRAEGER'
|
||||
type FederalState = 'NI' | 'NRW' | 'BAY' | 'BW' | 'HE' | 'SN' | 'TH' | 'SA' | 'BB' | 'MV' | 'SH' | 'HH' | 'HB' | 'BE' | 'SL' | 'RP'
|
||||
|
||||
interface FormData {
|
||||
title: string
|
||||
funding_program: FundingProgram
|
||||
federal_state: FederalState
|
||||
preset_id: string
|
||||
}
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8080'
|
||||
|
||||
const fundingPrograms = [
|
||||
{ value: 'DIGITALPAKT_2', label: 'DigitalPakt 2.0', description: 'Foerderung digitaler Bildungsinfrastruktur (2025-2030)' },
|
||||
{ value: 'DIGITALPAKT_1', label: 'DigitalPakt 1.0 (Restmittel)', description: 'Restmittel aus der ersten Phase' },
|
||||
{ value: 'LANDESFOERDERUNG', label: 'Landesfoerderung', description: 'Landesspezifische Foerderprogramme' },
|
||||
{ value: 'SCHULTRAEGER', label: 'Schultraegerfoerderung', description: 'Foerderung durch Schultraeger' },
|
||||
]
|
||||
|
||||
const federalStates = [
|
||||
{ value: 'NI', label: 'Niedersachsen', flag: 'NI' },
|
||||
{ value: 'NRW', label: 'Nordrhein-Westfalen', flag: 'NRW' },
|
||||
{ value: 'BAY', label: 'Bayern', flag: 'BAY' },
|
||||
{ value: 'BW', label: 'Baden-Wuerttemberg', flag: 'BW' },
|
||||
{ value: 'HE', label: 'Hessen', flag: 'HE' },
|
||||
{ value: 'SN', label: 'Sachsen', flag: 'SN' },
|
||||
{ value: 'TH', label: 'Thueringen', flag: 'TH' },
|
||||
{ value: 'SA', label: 'Sachsen-Anhalt', flag: 'SA' },
|
||||
{ value: 'BB', label: 'Brandenburg', flag: 'BB' },
|
||||
{ value: 'MV', label: 'Mecklenburg-Vorpommern', flag: 'MV' },
|
||||
{ value: 'SH', label: 'Schleswig-Holstein', flag: 'SH' },
|
||||
{ value: 'HH', label: 'Hamburg', flag: 'HH' },
|
||||
{ value: 'HB', label: 'Bremen', flag: 'HB' },
|
||||
{ value: 'BE', label: 'Berlin', flag: 'BE' },
|
||||
{ value: 'SL', label: 'Saarland', flag: 'SL' },
|
||||
{ value: 'RP', label: 'Rheinland-Pfalz', flag: 'RP' },
|
||||
]
|
||||
|
||||
const presets = [
|
||||
{
|
||||
id: 'breakpilot_basic',
|
||||
name: 'BreakPilot Basis',
|
||||
description: 'Lokale KI-Arbeitsstation fuer eine Schule',
|
||||
budget: '~18.500 EUR',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'breakpilot_cluster',
|
||||
name: 'BreakPilot Schulverbund',
|
||||
description: 'Zentrale KI-Infrastruktur fuer mehrere Schulen',
|
||||
budget: '~68.500 EUR',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
),
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
name: 'Individuell',
|
||||
description: 'Leerer Wizard fuer eigene Projekte',
|
||||
budget: 'Flexibel',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
</svg>
|
||||
),
|
||||
color: 'slate',
|
||||
},
|
||||
]
|
||||
|
||||
export default function NewFoerderantragPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
title: '',
|
||||
funding_program: 'DIGITALPAKT_2',
|
||||
federal_state: 'NI',
|
||||
preset_id: searchParams.get('preset') || '',
|
||||
})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Set preset from URL params
|
||||
useEffect(() => {
|
||||
const preset = searchParams.get('preset')
|
||||
if (preset) {
|
||||
setFormData(prev => ({ ...prev, preset_id: preset }))
|
||||
// Auto-generate title based on preset
|
||||
const presetInfo = presets.find(p => p.id === preset)
|
||||
if (presetInfo && presetInfo.id) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
preset_id: preset,
|
||||
title: `${presetInfo.name} - ${new Date().toLocaleDateString('de-DE')}`,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
|
||||
if (!formData.title.trim()) {
|
||||
setError('Bitte geben Sie einen Projekttitel ein')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// In production, this would call the API
|
||||
// const response = await fetch(`${API_BASE}/sdk/v1/funding/applications`, {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify(formData),
|
||||
// })
|
||||
// const data = await response.json()
|
||||
// router.push(`/education/foerderantrag/${data.id}`)
|
||||
|
||||
// For now, redirect to mock ID
|
||||
const mockId = 'demo-' + Date.now()
|
||||
router.push(`/education/foerderantrag/${mockId}`)
|
||||
} catch (err) {
|
||||
setError('Fehler beim Erstellen des Antrags')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getPresetColorClasses = (color: string, isSelected: boolean) => {
|
||||
const colors: Record<string, { border: string; bg: string; ring: string }> = {
|
||||
blue: {
|
||||
border: isSelected ? 'border-blue-500' : 'border-slate-200',
|
||||
bg: isSelected ? 'bg-blue-50' : 'bg-white',
|
||||
ring: 'ring-blue-500',
|
||||
},
|
||||
purple: {
|
||||
border: isSelected ? 'border-purple-500' : 'border-slate-200',
|
||||
bg: isSelected ? 'bg-purple-50' : 'bg-white',
|
||||
ring: 'ring-purple-500',
|
||||
},
|
||||
slate: {
|
||||
border: isSelected ? 'border-slate-500' : 'border-slate-200',
|
||||
bg: isSelected ? 'bg-slate-50' : 'bg-white',
|
||||
ring: 'ring-slate-500',
|
||||
},
|
||||
}
|
||||
return colors[color] || colors.slate
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
href="/education/foerderantrag"
|
||||
className="inline-flex items-center gap-2 text-slate-600 hover:text-slate-900 mb-6"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Zurueck zur Uebersicht
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-900">Neuen Foerderantrag starten</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Waehlen Sie das Foerderprogramm und Ihr Bundesland. Der Wizard fuehrt Sie durch alle weiteren Schritte.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Preset Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||||
Schnellstart mit Preset (optional)
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{presets.map((preset) => {
|
||||
const isSelected = formData.preset_id === preset.id
|
||||
const colors = getPresetColorClasses(preset.color, isSelected)
|
||||
return (
|
||||
<button
|
||||
key={preset.id || 'custom'}
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({
|
||||
...prev,
|
||||
preset_id: preset.id,
|
||||
title: preset.id ? `${preset.name} - ${new Date().toLocaleDateString('de-DE')}` : prev.title,
|
||||
}))}
|
||||
className={`relative p-4 rounded-xl border-2 text-left transition-all ${colors.border} ${colors.bg} ${isSelected ? 'ring-2 ' + colors.ring : ''}`}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className={`w-12 h-12 rounded-lg bg-${preset.color}-100 text-${preset.color}-600 flex items-center justify-center mb-3`}>
|
||||
{preset.icon}
|
||||
</div>
|
||||
<h3 className="font-semibold text-slate-900">{preset.name}</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">{preset.description}</p>
|
||||
<p className="text-sm font-medium text-slate-700 mt-2">{preset.budget}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Funding Program */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||||
Foerderprogramm *
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{fundingPrograms.map((program) => (
|
||||
<label
|
||||
key={program.value}
|
||||
className={`relative flex items-start p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
||||
formData.funding_program === program.value
|
||||
? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="funding_program"
|
||||
value={program.value}
|
||||
checked={formData.funding_program === program.value}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, funding_program: e.target.value as FundingProgram }))}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-slate-900">{program.label}</span>
|
||||
{formData.funding_program === program.value && (
|
||||
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-1">{program.description}</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Federal State */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||||
Bundesland *
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{federalStates.map((state) => (
|
||||
<button
|
||||
key={state.value}
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({ ...prev, federal_state: state.value as FederalState }))}
|
||||
className={`px-4 py-3 rounded-lg border-2 text-sm font-medium transition-all ${
|
||||
formData.federal_state === state.value
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{state.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
{formData.federal_state === 'NI' && 'Niedersachsen ist der Pilot-Standort mit optimaler Unterstuetzung.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Project Title */}
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Projekttitel *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||
placeholder="z.B. Digitale Lernumgebung fuer differenzierten Unterricht"
|
||||
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
maxLength={200}
|
||||
/>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
Ein aussagekraeftiger Titel fuer Ihr Foerderprojekt (max. 200 Zeichen)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-xl text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-slate-200">
|
||||
<Link
|
||||
href="/education/foerderantrag"
|
||||
className="px-6 py-3 text-slate-600 hover:text-slate-900 font-medium"
|
||||
>
|
||||
Abbrechen
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-8 py-3 bg-blue-600 text-white rounded-xl font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Wird erstellt...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Wizard starten
|
||||
<svg className="w-4 h-4" 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>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Help Box */}
|
||||
<div className="mt-8 bg-amber-50 border border-amber-200 rounded-xl p-6">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="w-6 h-6 text-amber-600" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-amber-800">KI-Assistent verfuegbar</h3>
|
||||
<p className="mt-1 text-sm text-amber-700">
|
||||
Im Wizard steht Ihnen ein KI-Assistent zur Seite, der bei Fragen hilft,
|
||||
Formulierungen vorschlaegt und Sie durch den Antragsprozess fuehrt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
365
admin-v2/app/(admin)/education/foerderantrag/page.tsx
Normal file
365
admin-v2/app/(admin)/education/foerderantrag/page.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// Types
|
||||
interface FundingApplication {
|
||||
id: string
|
||||
application_number: string
|
||||
title: string
|
||||
funding_program: string
|
||||
status: string
|
||||
current_step: number
|
||||
total_steps: number
|
||||
requested_amount: number
|
||||
school_profile?: {
|
||||
name: string
|
||||
federal_state: string
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface Statistics {
|
||||
total_applications: number
|
||||
draft_count: number
|
||||
submitted_count: number
|
||||
approved_count: number
|
||||
total_requested: number
|
||||
total_approved: number
|
||||
}
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8080'
|
||||
|
||||
// Status badge colors
|
||||
const statusColors: Record<string, { bg: string; text: string; label: string }> = {
|
||||
DRAFT: { bg: 'bg-slate-100', text: 'text-slate-700', label: 'Entwurf' },
|
||||
IN_PROGRESS: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'In Bearbeitung' },
|
||||
REVIEW: { bg: 'bg-amber-100', text: 'text-amber-700', label: 'Pruefung' },
|
||||
SUBMITTED: { bg: 'bg-purple-100', text: 'text-purple-700', label: 'Eingereicht' },
|
||||
APPROVED: { bg: 'bg-green-100', text: 'text-green-700', label: 'Genehmigt' },
|
||||
REJECTED: { bg: 'bg-red-100', text: 'text-red-700', label: 'Abgelehnt' },
|
||||
}
|
||||
|
||||
const programLabels: Record<string, string> = {
|
||||
DIGITALPAKT_1: 'DigitalPakt 1.0',
|
||||
DIGITALPAKT_2: 'DigitalPakt 2.0',
|
||||
LANDESFOERDERUNG: 'Landesfoerderung',
|
||||
SCHULTRAEGER: 'Schultraeger',
|
||||
}
|
||||
|
||||
export default function FoerderantragPage() {
|
||||
const [applications, setApplications] = useState<FundingApplication[]>([])
|
||||
const [statistics, setStatistics] = useState<Statistics | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// In production, these would be real API calls
|
||||
// For now, we use mock data
|
||||
setApplications([])
|
||||
setStatistics({
|
||||
total_applications: 0,
|
||||
draft_count: 0,
|
||||
submitted_count: 0,
|
||||
approved_count: 0,
|
||||
total_requested: 0,
|
||||
total_approved: 0,
|
||||
})
|
||||
} catch (err) {
|
||||
setError('Fehler beim Laden der Daten')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Foerderantrag-Wizard"
|
||||
purpose="Erstellen Sie antragsfaehige Foerderantraege fuer Schulen. Der Wizard fuehrt Sie Schritt fuer Schritt durch den Prozess und generiert alle erforderlichen Dokumente."
|
||||
audience={['Schulleitung', 'IT-Beauftragte', 'Schultraeger']}
|
||||
architecture={{
|
||||
services: ['ai-compliance-sdk (Go)', 'LLM-Service (32B)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800 p-8 text-white">
|
||||
<div className="absolute inset-0 bg-[url('/grid-pattern.svg')] opacity-10" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Foerderantrag-Wizard</h1>
|
||||
<p className="mt-2 text-blue-100 max-w-2xl">
|
||||
Erstellen Sie vollstaendige Foerderantraege fuer DigitalPakt 2.0 und Landesfoerderungen.
|
||||
Der Wizard fuehrt Sie durch alle 8 Schritte und generiert antragsfaehige Dokumente.
|
||||
</p>
|
||||
<div className="mt-6 flex gap-4">
|
||||
<Link
|
||||
href="/education/foerderantrag/new"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-white text-blue-700 rounded-xl font-semibold hover:bg-blue-50 transition-colors shadow-lg"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neuen Antrag starten
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<svg className="w-32 h-32 text-blue-300 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">{statistics?.total_applications || 0}</div>
|
||||
<div className="text-sm text-slate-500">Antraege gesamt</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-amber-600" 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>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">{statistics?.draft_count || 0}</div>
|
||||
<div className="text-sm text-slate-500">Entwuerfe</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-purple-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">{statistics?.submitted_count || 0}</div>
|
||||
<div className="text-sm text-slate-500">Eingereicht</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">{formatCurrency(statistics?.total_requested || 0)}</div>
|
||||
<div className="text-sm text-slate-500">Beantragt</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Start Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Link
|
||||
href="/education/foerderantrag/new?preset=breakpilot_basic"
|
||||
className="group bg-white rounded-xl border-2 border-slate-200 p-6 hover:border-blue-400 hover:shadow-lg transition-all"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg text-slate-900 group-hover:text-blue-600 transition-colors">
|
||||
BreakPilot Basis
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Lokale KI-Arbeitsstation fuer eine Schule. Vorausgefuellte Kostenplanung und Datenschutzkonzept.
|
||||
</p>
|
||||
<div className="mt-4 text-sm font-medium text-blue-600">
|
||||
~18.500 EUR Foerdervolumen
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/education/foerderantrag/new?preset=breakpilot_cluster"
|
||||
className="group bg-white rounded-xl border-2 border-slate-200 p-6 hover:border-blue-400 hover:shadow-lg transition-all"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500 to-pink-600 flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg text-slate-900 group-hover:text-purple-600 transition-colors">
|
||||
BreakPilot Schulverbund
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Zentrale KI-Infrastruktur fuer mehrere Schulen eines Traegers.
|
||||
</p>
|
||||
<div className="mt-4 text-sm font-medium text-purple-600">
|
||||
~68.500 EUR Foerdervolumen
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/education/foerderantrag/new"
|
||||
className="group bg-white rounded-xl border-2 border-slate-200 p-6 hover:border-slate-400 hover:shadow-lg transition-all"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-slate-500 to-slate-700 flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg text-slate-900 group-hover:text-slate-700 transition-colors">
|
||||
Individueller Antrag
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Leerer Wizard fuer individuelle Projekte. Volle Flexibilitaet bei der Planung.
|
||||
</p>
|
||||
<div className="mt-4 text-sm font-medium text-slate-600">
|
||||
Beliebiges Foerdervolumen
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Applications List */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-lg text-slate-900">Meine Antraege</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<select className="px-3 py-1.5 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">Alle Status</option>
|
||||
<option value="DRAFT">Entwurf</option>
|
||||
<option value="SUBMITTED">Eingereicht</option>
|
||||
<option value="APPROVED">Genehmigt</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="p-12 text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-slate-500">Lade Antraege...</p>
|
||||
</div>
|
||||
) : applications.length === 0 ? (
|
||||
<div className="p-12 text-center">
|
||||
<svg className="w-16 h-16 text-slate-300 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 className="mt-4 text-lg font-medium text-slate-900">Noch keine Antraege</h3>
|
||||
<p className="mt-2 text-slate-500">
|
||||
Starten Sie jetzt Ihren ersten Foerderantrag mit dem Wizard.
|
||||
</p>
|
||||
<Link
|
||||
href="/education/foerderantrag/new"
|
||||
className="mt-6 inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Ersten Antrag erstellen
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-100">
|
||||
{applications.map((app) => {
|
||||
const status = statusColors[app.status] || statusColors.DRAFT
|
||||
return (
|
||||
<Link
|
||||
key={app.id}
|
||||
href={`/education/foerderantrag/${app.id}`}
|
||||
className="flex items-center gap-4 p-4 hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-medium text-slate-900 truncate">{app.title}</h3>
|
||||
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${status.bg} ${status.text}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-slate-500">
|
||||
<span>{app.application_number}</span>
|
||||
<span>{programLabels[app.funding_program] || app.funding_program}</span>
|
||||
{app.school_profile?.name && (
|
||||
<span>{app.school_profile.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-medium text-slate-900">{formatCurrency(app.requested_amount)}</div>
|
||||
<div className="text-sm text-slate-500">Schritt {app.current_step}/{app.total_steps}</div>
|
||||
</div>
|
||||
<svg className="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="w-6 h-6 text-blue-600" 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>
|
||||
<div>
|
||||
<h3 className="font-semibold text-blue-800">Wichtiger Hinweis</h3>
|
||||
<p className="mt-1 text-sm text-blue-700">
|
||||
Der Wizard erstellt einen <strong>antragsfaehigen Entwurf</strong>. Die finale Pruefung und
|
||||
Einreichung erfolgt durch den Schultraeger. Alle generierten Dokumente (Antragsschreiben,
|
||||
Kostenplan, Datenschutzkonzept) koennen als ZIP heruntergeladen werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
82
admin-v2/app/(admin)/education/page.tsx
Normal file
82
admin-v2/app/(admin)/education/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { getCategoryById } from '@/lib/navigation'
|
||||
import { ModuleCard } from '@/components/common/ModuleCard'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
export default function EducationPage() {
|
||||
const category = getCategoryById('education')
|
||||
|
||||
if (!category) {
|
||||
return <div>Kategorie nicht gefunden</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title={category.name}
|
||||
purpose="Diese Kategorie umfasst Module fuer Bildungsdokumente. Hier verwalten Sie Crawler fuer Lehrplaene, Erlasse und amtliche Bildungsquellen."
|
||||
audience={['Content Manager', 'Entwickler']}
|
||||
architecture={{
|
||||
services: ['edu-search-service (Go)'],
|
||||
databases: ['PostgreSQL', 'OpenSearch'],
|
||||
}}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
/>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-3xl font-bold text-blue-600">2</div>
|
||||
<div className="text-sm text-slate-500">Aktive Crawler</div>
|
||||
<div className="text-xs text-slate-400">NiBiS, KMK</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-3xl font-bold text-blue-600">16</div>
|
||||
<div className="text-sm text-slate-500">Bundeslaender</div>
|
||||
<div className="text-xs text-slate-400">Geplant</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-3xl font-bold text-green-600">0</div>
|
||||
<div className="text-sm text-slate-500">Personendaten</div>
|
||||
<div className="text-xs text-green-500">Datenschutz-konform</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modules Grid */}
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Module</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{category.modules.map((module) => (
|
||||
<ModuleCard key={module.id} module={module} category={category} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-8 bg-blue-50 border border-blue-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
<span>📚</span>
|
||||
Bildungsdokumente
|
||||
</h3>
|
||||
<p className="text-sm text-blue-700 mt-2">
|
||||
Das System crawlt ausschliesslich oeffentliche Bildungsdokumente (Lehrplaene, Erlasse, Beschluesse).
|
||||
<strong> Keine personenbezogenen Daten</strong> werden erfasst oder gespeichert.
|
||||
Alle Crawler respektieren robots.txt und verwenden Rate-Limiting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Compliance Note */}
|
||||
<div className="mt-4 bg-green-50 border border-green-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-green-800 flex items-center gap-2">
|
||||
<span>✅</span>
|
||||
Datenschutz-Hinweis
|
||||
</h3>
|
||||
<p className="text-sm text-green-700 mt-2">
|
||||
Dieses Modul verarbeitet <strong>keine personenbezogenen Daten</strong>.
|
||||
Es werden ausschliesslich amtliche Dokumente und Metadaten aus oeffentlichen Quellen indexiert.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user