Files
breakpilot-lehrer/studio-v2/app/alerts-b2b/page.tsx
Benjamin Admin 0b37c5e692 [split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split):
- compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20)
- quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11)
- i18n.ts (1,173 → 8 language files)
- unity-bridge (1,094 → 12), backlog (1,087 → 6)
- training (1,066 → 8), rag (1,063 → 8)
- Deleted index_original.ts (4,899 LOC dead backup)

Studio-v2 (5 monoliths split):
- meet/page.tsx (1,481 → 9), messages (1,166 → 9)
- AlertsB2BContext.tsx (1,165 → 5 modules)
- alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6)

All existing imports preserved. Zero new TypeScript errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 17:52:36 +02:00

299 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useTheme } from '@/lib/ThemeContext'
import { useLanguage } from '@/lib/LanguageContext'
import {
useAlertsB2B,
B2BHit,
ImportanceLabel,
getImportanceLabelColor,
getPackageIcon,
getPackageLabel
} from '@/lib/AlertsB2BContext'
import { Sidebar } from '@/components/Sidebar'
import { B2BMigrationWizard } from '@/components/B2BMigrationWizard'
import { TipBox } from '@/components/InfoBox'
import { Footer } from '@/components/Footer'
import { LanguageDropdown } from '@/components/LanguageDropdown'
import { ThemeToggle } from '@/components/ThemeToggle'
import { HitCard } from './_components/HitCard'
import { HitDetailModal } from './_components/HitDetailModal'
import { EmailImportModal } from './_components/EmailImportModal'
import { DigestView } from './_components/DigestView'
export default function AlertsB2BPage() {
const router = useRouter()
const { isDark } = useTheme()
const { t } = useLanguage()
const {
tenant,
settings,
updateSettings,
hits,
topics,
unreadCount,
relevantCount,
needsReviewCount,
markAsRead,
markAllAsRead,
submitFeedback,
processEmailContent,
getDigest
} = useAlertsB2B()
const [selectedHit, setSelectedHit] = useState<B2BHit | null>(null)
const [showWizard, setShowWizard] = useState(false)
const [showImportModal, setShowImportModal] = useState(false)
const [viewMode, setViewMode] = useState<'inbox' | 'digest'>('inbox')
const [filterDecision, setFilterDecision] = useState<string>('all')
const [filterImportance, setFilterImportance] = useState<string>('all')
const [importSuccess, setImportSuccess] = useState<B2BHit | null>(null)
useEffect(() => {
if (!settings.wizardCompleted) setShowWizard(true)
}, [settings.wizardCompleted])
const filteredHits = hits.filter(hit => {
if (filterDecision === 'unread') return !hit.isRead
if (filterDecision !== 'all' && hit.decisionLabel !== filterDecision) return false
if (filterImportance !== 'all' && hit.importanceLabel !== filterImportance) return false
return true
}).sort((a, b) => b.importanceScore - a.importanceScore)
const handleHitClick = (hit: B2BHit) => {
setSelectedHit(hit)
if (!hit.isRead) markAsRead(hit.id)
}
const handleFeedback = (hitId: string, feedback: 'relevant' | 'irrelevant') => {
submitFeedback(hitId, feedback)
}
if (showWizard) {
return (
<B2BMigrationWizard
onComplete={() => { updateSettings({ wizardCompleted: true, migrationCompleted: true }); setShowWizard(false) }}
onSkip={() => { updateSettings({ wizardCompleted: true }); setShowWizard(false) }}
onCancel={() => { router.push('/') }}
/>
)
}
return (
<div className={`min-h-screen relative overflow-hidden flex flex-col ${
isDark
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
}`}>
{/* Animated Background Blobs */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className={`absolute -top-40 -right-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${isDark ? 'bg-purple-500 opacity-70' : 'bg-purple-300 opacity-50'}`} />
<div className={`absolute -bottom-40 -left-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${isDark ? 'bg-blue-500 opacity-70' : 'bg-blue-300 opacity-50'}`} />
<div className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${isDark ? 'bg-pink-500 opacity-70' : 'bg-pink-300 opacity-50'}`} />
</div>
<div className="relative z-10 flex min-h-screen gap-6 p-4">
<Sidebar selectedTab="alerts-b2b" />
<main className="flex-1">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className={`text-4xl font-bold mb-2 flex items-center gap-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
<span>🏢</span> B2B Alerts
{unreadCount > 0 && (
<span className="px-3 py-1 text-sm font-medium rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30">
{unreadCount} neu
</span>
)}
</h1>
<p className={isDark ? 'text-white/60' : 'text-slate-600'}>{tenant.companyName} - Procurement Alerts</p>
</div>
<div className="flex items-center gap-4">
<div className={`flex rounded-xl overflow-hidden border ${isDark ? 'border-white/20' : 'border-slate-200'}`}>
{(['inbox', 'digest'] as const).map(mode => (
<button key={mode} onClick={() => setViewMode(mode)} className={`px-4 py-2 text-sm font-medium transition-all ${
viewMode === mode
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
: isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'
}`}>
{mode === 'inbox' ? 'Inbox' : 'Digest'}
</button>
))}
</div>
<LanguageDropdown />
<ThemeToggle />
<button onClick={() => setShowImportModal(true)} className={`px-4 py-2 backdrop-blur-xl border rounded-2xl transition-all flex items-center gap-2 ${isDark ? 'bg-purple-500/20 border-purple-500/30 text-purple-300 hover:bg-purple-500/30' : 'bg-purple-100 border-purple-200 text-purple-700 hover:bg-purple-200'}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" /></svg>
<span className="text-sm font-medium">E-Mail einfuegen</span>
</button>
<button onClick={() => setShowWizard(true)} className={`p-3 backdrop-blur-xl border rounded-2xl hover:bg-white/20 transition-all ${isDark ? 'bg-white/10 border-white/20 text-white' : 'bg-black/5 border-black/10 text-slate-700'}`}>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
</button>
</div>
</div>
<div className="grid grid-cols-3 gap-6">
{/* Main Panel (2/3) */}
<div className={`col-span-2 backdrop-blur-xl border rounded-3xl p-6 ${isDark ? 'bg-white/10 border-white/20' : 'bg-white/70 border-black/10 shadow-lg'}`}>
{viewMode === 'inbox' ? (
<>
{/* Decision Filter */}
<div className="flex items-center justify-between mb-6 flex-wrap gap-3">
<div className="flex items-center gap-2 flex-wrap">
{['all', 'unread', 'relevant', 'needs_review', 'irrelevant'].map((filter) => (
<button key={filter} onClick={() => setFilterDecision(filter)} className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filterDecision === filter ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30' : isDark ? 'text-white/60 hover:text-white hover:bg-white/10' : 'text-slate-500 hover:text-slate-900 hover:bg-slate-100'}`}>
{filter === 'all' ? 'Alle' : filter === 'unread' ? 'Ungelesen' : filter === 'relevant' ? 'Relevant' : filter === 'needs_review' ? 'Pruefung' : 'Irrelevant'}
</button>
))}
</div>
{unreadCount > 0 && (
<button onClick={markAllAsRead} className={`text-sm ${isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'}`}>Alle gelesen</button>
)}
</div>
{/* Importance Filter */}
<div className="flex items-center gap-2 mb-6 flex-wrap">
<span className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Wichtigkeit:</span>
{['all', 'KRITISCH', 'DRINGEND', 'WICHTIG', 'PRUEFEN', 'INFO'].map((filter) => (
<button key={filter} onClick={() => setFilterImportance(filter)} className={`px-2 py-1 rounded text-xs transition-all ${filterImportance === filter ? filter === 'all' ? 'bg-purple-500/20 text-purple-400' : getImportanceLabelColor(filter as ImportanceLabel, isDark) : isDark ? 'text-white/40 hover:text-white/60' : 'text-slate-400 hover:text-slate-600'}`}>
{filter === 'all' ? 'Alle' : filter}
</button>
))}
</div>
{/* Hits List */}
{filteredHits.length === 0 ? (
<div className={`text-center py-12 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<span className="text-4xl block mb-4">📭</span><p>Keine Hits gefunden</p>
</div>
) : (
<div className="space-y-3">
{filteredHits.map(hit => <HitCard key={hit.id} hit={hit} onClick={() => handleHitClick(hit)} />)}
</div>
)}
</>
) : (
<DigestView hits={getDigest()} onHitClick={handleHitClick} />
)}
</div>
{/* Right Sidebar (1/3) */}
<div className="space-y-6">
{/* Stats */}
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${isDark ? 'bg-white/10 border-white/20' : 'bg-white/70 border-black/10 shadow-lg'}`}>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}><span>📊</span> Statistik</h2>
<div className="space-y-3">
{[
{ label: 'Gesamt Hits', value: hits.length, color: isDark ? 'text-white' : 'text-slate-900' },
{ label: 'Ungelesen', value: unreadCount, color: 'text-purple-500' },
{ label: 'Relevant', value: relevantCount, color: 'text-green-500' },
{ label: 'Pruefung noetig', value: needsReviewCount, color: 'text-amber-500' },
].map(({ label, value, color }) => (
<div key={label} className="flex items-center justify-between">
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>{label}</span>
<span className={`font-medium ${color}`}>{value}</span>
</div>
))}
</div>
</div>
{/* Topics */}
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${isDark ? 'bg-white/10 border-white/20' : 'bg-white/70 border-black/10 shadow-lg'}`}>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}><span>🎯</span> Topics</h2>
{topics.length === 0 ? (
<p className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Keine Topics konfiguriert.</p>
) : (
<div className="space-y-2">
{topics.map(topic => (
<div key={topic.id} className={`flex items-center gap-3 p-3 rounded-lg ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
<span className="text-lg">{getPackageIcon(topic.package)}</span>
<div className="flex-1 min-w-0">
<p className={`font-medium text-sm truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>{topic.name}</p>
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>{getPackageLabel(topic.package)}</p>
</div>
<div className={`w-2 h-2 rounded-full ${topic.status === 'active' ? 'bg-green-500' : 'bg-slate-400'}`} />
</div>
))}
</div>
)}
</div>
{/* Settings Summary */}
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${isDark ? 'bg-white/10 border-white/20' : 'bg-white/70 border-black/10 shadow-lg'}`}>
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}><span></span> Einstellungen</h2>
<div className="space-y-3 text-sm">
<div className="flex items-center justify-between">
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Benachrichtigung</span>
<span className={isDark ? 'text-white' : 'text-slate-900'}>{settings.notificationCadence}</span>
</div>
<div className="flex items-center justify-between">
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Noise-Modus</span>
<span className={`px-2 py-0.5 rounded text-xs ${settings.noiseMode === 'STRICT' ? 'bg-green-500/20 text-green-400' : settings.noiseMode === 'BALANCED' ? 'bg-amber-500/20 text-amber-400' : 'bg-red-500/20 text-red-400'}`}>{settings.noiseMode}</span>
</div>
<div className="flex items-center justify-between">
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Max. Digest</span>
<span className={isDark ? 'text-white' : 'text-slate-900'}>{settings.maxDigestItems} Items</span>
</div>
</div>
</div>
<TipBox title="Noise-Reduktion" icon="🎯">
<p className="text-sm">Im STRICT-Modus werden nur ~10% der Hits als relevant markiert. Der Rest wird automatisch gefiltert.</p>
</TipBox>
</div>
</div>
</main>
</div>
{selectedHit && <HitDetailModal hit={selectedHit} onClose={() => setSelectedHit(null)} onFeedback={(feedback) => handleFeedback(selectedHit.id, feedback)} />}
{showImportModal && (
<EmailImportModal
onClose={() => setShowImportModal(false)}
onImport={(content, subject) => {
const newHit = processEmailContent(content, subject)
setImportSuccess(newHit)
setTimeout(() => setImportSuccess(null), 5000)
}}
/>
)}
{importSuccess && (
<div className="fixed bottom-6 right-6 z-50 animate-fade-in">
<div className={`p-4 rounded-2xl border shadow-xl backdrop-blur-xl max-w-md ${isDark ? 'bg-green-500/20 border-green-500/30' : 'bg-green-50 border-green-200 shadow-green-100'}`}>
<div className="flex items-start gap-3">
<span className="text-2xl"></span>
<div className="flex-1">
<p className={`font-medium ${isDark ? 'text-green-300' : 'text-green-700'}`}>E-Mail erfolgreich importiert!</p>
<p className={`text-sm mt-1 ${isDark ? 'text-green-200/70' : 'text-green-600'}`}>{importSuccess.title.slice(0, 60)}...</p>
<div className="flex items-center gap-2 mt-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${importSuccess.decisionLabel === 'relevant' ? 'bg-green-500/20 text-green-400' : importSuccess.decisionLabel === 'needs_review' ? 'bg-amber-500/20 text-amber-400' : 'bg-slate-500/20 text-slate-400'}`}>
{importSuccess.decisionLabel === 'relevant' ? 'Relevant' : importSuccess.decisionLabel === 'needs_review' ? 'Pruefung' : 'Info'}
</span>
<span className={`text-xs ${isDark ? 'text-green-200/50' : 'text-green-500'}`}>Score: {importSuccess.importanceScore}</span>
</div>
</div>
<button onClick={() => setImportSuccess(null)} className={`p-1 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-green-100'}`}>
<svg className={`w-4 h-4 ${isDark ? 'text-green-300' : 'text-green-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
</div>
</div>
)}
<Footer />
<style jsx>{`
@keyframes blob { 0% { transform: translate(0px, 0px) scale(1); } 33% { transform: translate(30px, -50px) scale(1.1); } 66% { transform: translate(-20px, 20px) scale(0.9); } 100% { transform: translate(0px, 0px) scale(1); } }
.animate-blob { animation: blob 7s infinite; }
.animation-delay-2000 { animation-delay: 2s; }
.animation-delay-4000 { animation-delay: 4s; }
@keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-in { animation: fade-in 0.3s ease-out; }
`}</style>
</div>
)
}