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>
299 lines
18 KiB
TypeScript
299 lines
18 KiB
TypeScript
'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>
|
||
)
|
||
}
|