backend-lehrer (10 files): - game/database.py (785 → 5), correction_api.py (683 → 4) - classroom_engine/antizipation.py (676 → 5) - llm_gateway schools/edu_search already done in prior batch klausur-service (12 files): - orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4) - zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5) - eh_templates.py (658 → 5), mail/api.py (651 → 5) - qdrant_service.py (638 → 5), training_api.py (625 → 4) website (6 pages): - middleware (696 → 8), mail (733 → 6), consent (628 → 8) - compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7) studio-v2 (3 components): - B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2) - dashboard-experimental (739 → 2) admin-lehrer (4 files): - uebersetzungen (769 → 4), manager (670 → 2) - ChunkBrowserQA (675 → 6), dsfa/page (674 → 5) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
461 lines
17 KiB
TypeScript
461 lines
17 KiB
TypeScript
'use client'
|
|
|
|
import { useTheme } from '@/lib/ThemeContext'
|
|
import { B2BTemplate, Package, getPackageIcon, getPackageLabel } from '@/lib/AlertsB2BContext'
|
|
import { InfoBox, TipBox, StepBox } from './InfoBox'
|
|
import type { MigrationMethod } from './B2BWizardSteps'
|
|
|
|
// =============================================================================
|
|
// Step 4: Migration Details (Email / RSS / Reconstruct)
|
|
// =============================================================================
|
|
|
|
interface Step4Props {
|
|
migrationMethod: MigrationMethod
|
|
setMigrationMethod: (v: MigrationMethod) => void
|
|
inboundEmail: string
|
|
testEmailSent: boolean
|
|
setTestEmailSent: (v: boolean) => void
|
|
rssUrls: string[]
|
|
setRssUrls: (v: string[]) => void
|
|
alertDescription: string
|
|
setAlertDescription: (v: string) => void
|
|
}
|
|
|
|
export function WizardStep4({
|
|
migrationMethod,
|
|
setMigrationMethod,
|
|
inboundEmail,
|
|
testEmailSent,
|
|
setTestEmailSent,
|
|
rssUrls,
|
|
setRssUrls,
|
|
alertDescription,
|
|
setAlertDescription,
|
|
}: Step4Props) {
|
|
const { isDark } = useTheme()
|
|
|
|
return (
|
|
<div>
|
|
{/* Email Forwarding */}
|
|
{migrationMethod === 'email' && (
|
|
<EmailForwardingDetails
|
|
inboundEmail={inboundEmail}
|
|
testEmailSent={testEmailSent}
|
|
setTestEmailSent={setTestEmailSent}
|
|
/>
|
|
)}
|
|
|
|
{/* RSS Import */}
|
|
{migrationMethod === 'rss' && (
|
|
<RSSImportDetails
|
|
rssUrls={rssUrls}
|
|
setRssUrls={setRssUrls}
|
|
setMigrationMethod={setMigrationMethod}
|
|
/>
|
|
)}
|
|
|
|
{/* Reconstruction */}
|
|
{migrationMethod === 'reconstruct' && (
|
|
<ReconstructionDetails
|
|
alertDescription={alertDescription}
|
|
setAlertDescription={setAlertDescription}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Email Forwarding Details
|
|
// =============================================================================
|
|
|
|
function EmailForwardingDetails({
|
|
inboundEmail,
|
|
testEmailSent,
|
|
setTestEmailSent,
|
|
}: {
|
|
inboundEmail: string
|
|
testEmailSent: boolean
|
|
setTestEmailSent: (v: boolean) => void
|
|
}) {
|
|
const { isDark } = useTheme()
|
|
return (
|
|
<>
|
|
<h2 className={`text-2xl font-bold mb-2 text-center ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
E-Mail Weiterleitung einrichten
|
|
</h2>
|
|
<p className={`mb-6 text-center ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
|
|
Google Alerts sendet E-Mails - leiten Sie diese einfach an uns weiter
|
|
</p>
|
|
|
|
<InfoBox variant="info" title="So funktioniert es" icon="💡" className="mb-6">
|
|
<p>
|
|
Google Alerts versendet Benachrichtigungen per E-Mail an Ihr Konto.
|
|
Sie richten einen Gmail-Filter ein, der diese E-Mails automatisch weiterleitet -
|
|
wir uebernehmen die Verarbeitung und Auswertung.
|
|
</p>
|
|
</InfoBox>
|
|
|
|
<div className="space-y-6">
|
|
{/* Inbound Email */}
|
|
<div className={`p-4 rounded-xl border-2 ${isDark ? 'bg-green-500/10 border-green-500/30' : 'bg-green-50 border-green-200'}`}>
|
|
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
|
Ihre eindeutige Weiterleitungsadresse:
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
readOnly
|
|
value={inboundEmail}
|
|
className={`flex-1 px-4 py-3 rounded-lg border font-mono text-sm ${
|
|
isDark
|
|
? 'bg-white/5 border-white/20 text-white'
|
|
: 'bg-white border-slate-200 text-slate-900'
|
|
}`}
|
|
/>
|
|
<button
|
|
onClick={() => navigator.clipboard.writeText(inboundEmail)}
|
|
className="px-4 py-2 rounded-lg bg-purple-500 text-white hover:bg-purple-600 transition-all"
|
|
>
|
|
Kopieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Steps */}
|
|
<div className="space-y-3">
|
|
<StepBox step={1} title="Gmail-Einstellungen oeffnen" isActive>
|
|
Oeffnen Sie <a href="https://mail.google.com/mail/u/0/#settings/filters" target="_blank" rel="noopener noreferrer" className="text-purple-400 hover:underline">Gmail → Einstellungen → Filter und blockierte Adressen</a>
|
|
</StepBox>
|
|
<StepBox step={2} title="Neuen Filter erstellen">
|
|
Klicken Sie auf "Neuen Filter erstellen" und geben Sie bei "Von" ein: <code className={`px-2 py-1 rounded ${isDark ? 'bg-white/10' : 'bg-slate-100'}`}>googlealerts-noreply@google.com</code>
|
|
</StepBox>
|
|
<StepBox step={3} title="Weiterleitung aktivieren">
|
|
Waehlen Sie "Weiterleiten an" und fuegen Sie die obige Adresse ein. Aktivieren Sie auch "Filter auf passende Konversationen anwenden".
|
|
</StepBox>
|
|
</div>
|
|
|
|
<TipBox title="Keine Aenderung an Ihren Google Alerts noetig" icon="✨" className="mt-4">
|
|
<p>
|
|
Ihre bestehenden Google Alerts bleiben unveraendert. Der Gmail-Filter leitet
|
|
eingehende Alert-E-Mails automatisch an uns weiter. Sie koennen die E-Mails
|
|
auch weiterhin in Ihrem Posteingang sehen.
|
|
</p>
|
|
</TipBox>
|
|
|
|
{/* Test Button */}
|
|
<div className={`p-4 rounded-xl border ${
|
|
testEmailSent
|
|
? isDark ? 'bg-green-500/10 border-green-500/30' : 'bg-green-50 border-green-200'
|
|
: isDark ? 'bg-white/5 border-white/10' : 'bg-white border-slate-200'
|
|
}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
{testEmailSent ? '✓ Test-E-Mail empfangen' : 'Verbindung testen'}
|
|
</p>
|
|
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
{testEmailSent
|
|
? 'Die Weiterleitung funktioniert!'
|
|
: 'Senden Sie eine Test-E-Mail, um die Einrichtung zu pruefen'}
|
|
</p>
|
|
</div>
|
|
{!testEmailSent && (
|
|
<button
|
|
onClick={() => setTestEmailSent(true)}
|
|
className="px-4 py-2 rounded-lg bg-white/20 hover:bg-white/30 transition-all"
|
|
>
|
|
Test senden
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// RSS Import Details
|
|
// =============================================================================
|
|
|
|
function RSSImportDetails({
|
|
rssUrls,
|
|
setRssUrls,
|
|
setMigrationMethod,
|
|
}: {
|
|
rssUrls: string[]
|
|
setRssUrls: (v: string[]) => void
|
|
setMigrationMethod: (v: MigrationMethod) => void
|
|
}) {
|
|
const { isDark } = useTheme()
|
|
return (
|
|
<>
|
|
<h2 className={`text-2xl font-bold mb-2 text-center ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
RSS-Feeds importieren
|
|
</h2>
|
|
<p className={`mb-6 text-center ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
|
|
Fuegen Sie die RSS-URLs Ihrer Google Alerts hinzu
|
|
</p>
|
|
|
|
{/* Warning Box */}
|
|
<InfoBox variant="warning" title="Wichtiger Hinweis zu RSS" icon="⚠️" className="mb-6">
|
|
<p>
|
|
Google hat die RSS-Option fuer viele Konten entfernt. Falls Sie in Google Alerts
|
|
kein RSS-Symbol sehen oder die Option "RSS-Feed" nicht verfuegbar ist,
|
|
nutzen Sie bitte stattdessen die <strong>E-Mail-Weiterleitung</strong>.
|
|
</p>
|
|
<button
|
|
onClick={() => setMigrationMethod('email')}
|
|
className="mt-3 text-sm font-medium text-purple-400 hover:text-purple-300 underline"
|
|
>
|
|
→ Zur E-Mail-Weiterleitung wechseln
|
|
</button>
|
|
</InfoBox>
|
|
|
|
<div className="space-y-4">
|
|
{rssUrls.map((url, idx) => (
|
|
<div key={idx} className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
placeholder="https://www.google.de/alerts/feeds/..."
|
|
value={url}
|
|
onChange={(e) => {
|
|
const newUrls = [...rssUrls]
|
|
newUrls[idx] = e.target.value
|
|
setRssUrls(newUrls)
|
|
}}
|
|
className={`flex-1 px-4 py-3 rounded-lg border ${
|
|
isDark
|
|
? 'bg-white/10 border-white/20 text-white placeholder-white/40'
|
|
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
|
|
}`}
|
|
/>
|
|
{rssUrls.length > 1 && (
|
|
<button
|
|
onClick={() => setRssUrls(rssUrls.filter((_, i) => i !== idx))}
|
|
className={`p-3 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
|
|
>
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<button
|
|
onClick={() => setRssUrls([...rssUrls, ''])}
|
|
className={`w-full py-3 rounded-lg border-2 border-dashed transition-all ${
|
|
isDark
|
|
? 'border-white/20 text-white/60 hover:border-white/40 hover:text-white'
|
|
: 'border-slate-200 text-slate-500 hover:border-slate-300 hover:text-slate-700'
|
|
}`}
|
|
>
|
|
+ Weiteren Feed hinzufuegen
|
|
</button>
|
|
|
|
<div className={`p-4 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
|
|
<p className={`text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
|
Falls RSS verfuegbar ist:
|
|
</p>
|
|
<ol className={`text-sm space-y-1 ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
<li>1. Oeffnen Sie google.de/alerts</li>
|
|
<li>2. Suchen Sie nach einem orangefarbenen RSS-Symbol</li>
|
|
<li>3. Klicken Sie darauf und kopieren Sie die URL</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Reconstruction Details
|
|
// =============================================================================
|
|
|
|
function ReconstructionDetails({
|
|
alertDescription,
|
|
setAlertDescription,
|
|
}: {
|
|
alertDescription: string
|
|
setAlertDescription: (v: string) => void
|
|
}) {
|
|
const { isDark } = useTheme()
|
|
return (
|
|
<>
|
|
<h2 className={`text-2xl font-bold mb-2 text-center ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
Was moechten Sie beobachten?
|
|
</h2>
|
|
<p className={`mb-6 text-center ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
|
|
Beschreiben Sie Ihre Beobachtungsziele - wir erstellen die optimale Konfiguration
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
<textarea
|
|
placeholder="z.B. Wir beobachten europaweite kommunale Ausschreibungen für Parkscheinautomaten, EV-Ladesäulen mit Bezahlterminals und Tankautomaten. Wir bekommen aktuell zu viele irrelevante Treffer wie News, Stellenanzeigen und Zubehör..."
|
|
value={alertDescription}
|
|
onChange={(e) => setAlertDescription(e.target.value)}
|
|
rows={6}
|
|
className={`w-full px-4 py-3 rounded-xl border resize-none ${
|
|
isDark
|
|
? 'bg-white/10 border-white/20 text-white placeholder-white/40'
|
|
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
|
|
}`}
|
|
/>
|
|
|
|
<InfoBox variant="tip" title="Je mehr Details, desto besser" icon="✨">
|
|
<p>Beschreiben Sie:</p>
|
|
<ul className="list-disc list-inside mt-2 space-y-1">
|
|
<li>Welche Produkte/Services Sie anbieten</li>
|
|
<li>Welche Kaeufer/Maerkte relevant sind</li>
|
|
<li>Was Sie aktuell stoert (zu viel News, Jobs, etc.)</li>
|
|
</ul>
|
|
</InfoBox>
|
|
|
|
{alertDescription.length > 50 && (
|
|
<div className={`p-4 rounded-xl ${isDark ? 'bg-green-500/10 border border-green-500/30' : 'bg-green-50 border border-green-200'}`}>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">🤖</span>
|
|
<div>
|
|
<p className={`font-medium ${isDark ? 'text-green-300' : 'text-green-700'}`}>
|
|
KI-Analyse bereit
|
|
</p>
|
|
<p className={`text-sm ${isDark ? 'text-green-300/70' : 'text-green-600'}`}>
|
|
Wir werden Ihre Beschreibung analysieren und optimale Filter erstellen.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Step 5: Notification Settings & Summary
|
|
// =============================================================================
|
|
|
|
interface Step5Props {
|
|
selectedTemplate: B2BTemplate | undefined
|
|
selectedRegions: string[]
|
|
setSelectedRegions: (v: string[]) => void
|
|
selectedPackages: string[]
|
|
setSelectedPackages: (v: string[]) => void
|
|
companyName: string
|
|
migrationMethod: MigrationMethod
|
|
}
|
|
|
|
export function WizardStep5({
|
|
selectedTemplate,
|
|
selectedRegions,
|
|
setSelectedRegions,
|
|
selectedPackages,
|
|
setSelectedPackages,
|
|
companyName,
|
|
migrationMethod,
|
|
}: Step5Props) {
|
|
const { isDark } = useTheme()
|
|
return (
|
|
<div>
|
|
<h2 className={`text-2xl font-bold mb-2 text-center ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
Benachrichtigungen konfigurieren
|
|
</h2>
|
|
<p className={`mb-6 text-center ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
|
|
Wie moechten Sie ueber relevante Ausschreibungen informiert werden?
|
|
</p>
|
|
|
|
<div className="space-y-6">
|
|
{/* Regions */}
|
|
{selectedTemplate && (
|
|
<div>
|
|
<label className={`block text-sm font-medium mb-3 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
|
Regionen
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{selectedTemplate.guidedConfig.regionSelector.options.map(region => (
|
|
<button
|
|
key={region}
|
|
onClick={() => {
|
|
if (selectedRegions.includes(region)) {
|
|
setSelectedRegions(selectedRegions.filter(r => r !== region))
|
|
} else {
|
|
setSelectedRegions([...selectedRegions, region])
|
|
}
|
|
}}
|
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
|
selectedRegions.includes(region)
|
|
? 'bg-blue-500 text-white'
|
|
: isDark
|
|
? 'bg-white/10 text-white/60 hover:bg-white/20'
|
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
|
}`}
|
|
>
|
|
{region}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Packages */}
|
|
{selectedTemplate && (
|
|
<div>
|
|
<label className={`block text-sm font-medium mb-3 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
|
Produktbereiche
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{selectedTemplate.guidedConfig.packageSelector.options.map(pkg => (
|
|
<button
|
|
key={pkg}
|
|
onClick={() => {
|
|
if (selectedPackages.includes(pkg)) {
|
|
setSelectedPackages(selectedPackages.filter(p => p !== pkg))
|
|
} else {
|
|
setSelectedPackages([...selectedPackages, pkg])
|
|
}
|
|
}}
|
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
|
selectedPackages.includes(pkg)
|
|
? 'bg-blue-500 text-white'
|
|
: isDark
|
|
? 'bg-white/10 text-white/60 hover:bg-white/20'
|
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
|
}`}
|
|
>
|
|
{getPackageIcon(pkg)} {getPackageLabel(pkg)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Summary */}
|
|
<div className={`p-4 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
|
|
<h4 className={`font-medium mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
Ihre Konfiguration
|
|
</h4>
|
|
<ul className={`space-y-2 text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
<li>• Firma: <strong>{companyName}</strong></li>
|
|
<li>• Template: <strong>{selectedTemplate?.templateName || 'Eigene Konfiguration'}</strong></li>
|
|
<li>• Migration: <strong>{
|
|
migrationMethod === 'email' ? 'E-Mail Weiterleitung' :
|
|
migrationMethod === 'rss' ? 'RSS Import' : 'Rekonstruktion'
|
|
}</strong></li>
|
|
<li>• Regionen: <strong>{selectedRegions.join(', ')}</strong></li>
|
|
<li>• Produkte: <strong>{selectedPackages.map(p => getPackageLabel(p as Package)).join(', ')}</strong></li>
|
|
<li>• Digest: <strong>Taeglich um 08:00, max. 10 Treffer</strong></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<TipBox title="Bereit fuer den Start" icon="🚀">
|
|
<p>
|
|
Nach Abschluss werden wir Ihre Alerts analysieren und nur die wirklich
|
|
relevanten Ausschreibungen herausfiltern. Erwarten Sie ca. 80-90% weniger
|
|
irrelevante Treffer.
|
|
</p>
|
|
</TipBox>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|