Files
breakpilot-lehrer/studio-v2/lib/AlertsB2BContext.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

345 lines
10 KiB
TypeScript

'use client'
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'
import type {
AlertSource,
B2BHit,
B2BTopic,
B2BTemplate,
B2BTenant,
B2BSettings,
AlertsB2BContextType,
} from './alerts-b2b/types'
import { hectronicTemplate, mockHectronicHits, defaultB2BSettings, defaultTenant } from './alerts-b2b/template-data'
import { processEmailToHit } from './alerts-b2b/actions'
// Re-export all types and helpers for backward compatibility
export type {
ImportanceLabel,
DecisionLabel,
SourceType,
NoiseMode,
Package,
AlertSource,
B2BHit,
DecisionTrace,
B2BTopic,
TopicIntent,
TopicFilters,
DecisionPolicy,
ImportanceModel,
B2BTemplate,
GuidedConfig,
NotificationPreset,
B2BTenant,
B2BSettings,
} from './alerts-b2b/types'
export { hectronicTemplate } from './alerts-b2b/template-data'
export {
getImportanceLabelColor,
getDecisionLabelColor,
formatDeadline,
getPackageIcon,
getPackageLabel,
} from './alerts-b2b/helpers'
// ============================================
// CONTEXT
// ============================================
const AlertsB2BContext = createContext<AlertsB2BContextType | null>(null)
// LocalStorage Keys
const B2B_TENANT_KEY = 'bp_b2b_tenant'
const B2B_SETTINGS_KEY = 'bp_b2b_settings'
const B2B_SOURCES_KEY = 'bp_b2b_sources'
const B2B_TOPICS_KEY = 'bp_b2b_topics'
const B2B_HITS_KEY = 'bp_b2b_hits'
export function AlertsB2BProvider({ children }: { children: ReactNode }) {
const [tenant, setTenant] = useState<B2BTenant>(defaultTenant)
const [settings, setSettings] = useState<B2BSettings>(defaultB2BSettings)
const [sources, setSources] = useState<AlertSource[]>([])
const [topics, setTopics] = useState<B2BTopic[]>([])
const [hits, setHits] = useState<B2BHit[]>([])
const [selectedTemplate, setSelectedTemplate] = useState<B2BTemplate | null>(null)
const [isLoading] = useState(false)
const [error] = useState<string | null>(null)
const [mounted, setMounted] = useState(false)
// Available templates
const availableTemplates: B2BTemplate[] = [hectronicTemplate]
// Load from localStorage
useEffect(() => {
const storedTenant = localStorage.getItem(B2B_TENANT_KEY)
const storedSettings = localStorage.getItem(B2B_SETTINGS_KEY)
const storedSources = localStorage.getItem(B2B_SOURCES_KEY)
const storedTopics = localStorage.getItem(B2B_TOPICS_KEY)
const storedHits = localStorage.getItem(B2B_HITS_KEY)
if (storedTenant) {
try {
const parsed = JSON.parse(storedTenant)
setTenant({ ...defaultTenant, ...parsed, createdAt: new Date(parsed.createdAt) })
} catch (e) {
console.error('Error parsing tenant:', e)
}
}
if (storedSettings) {
try {
setSettings({ ...defaultB2BSettings, ...JSON.parse(storedSettings) })
} catch (e) {
console.error('Error parsing settings:', e)
}
}
if (storedSources) {
try {
const parsed = JSON.parse(storedSources)
setSources(parsed.map((s: any) => ({ ...s, createdAt: new Date(s.createdAt) })))
} catch (e) {
console.error('Error parsing sources:', e)
}
}
if (storedTopics) {
try {
const parsed = JSON.parse(storedTopics)
setTopics(parsed.map((t: any) => ({ ...t, createdAt: new Date(t.createdAt) })))
} catch (e) {
console.error('Error parsing topics:', e)
}
}
if (storedHits) {
try {
const parsed = JSON.parse(storedHits)
setHits(parsed.map((h: any) => ({
...h,
foundAt: new Date(h.foundAt),
createdAt: new Date(h.createdAt)
})))
} catch (e) {
console.error('Error parsing hits:', e)
setHits(mockHectronicHits)
}
} else {
setHits(mockHectronicHits)
}
setMounted(true)
}, [])
// Save to localStorage
useEffect(() => {
if (mounted) localStorage.setItem(B2B_TENANT_KEY, JSON.stringify(tenant))
}, [tenant, mounted])
useEffect(() => {
if (mounted) localStorage.setItem(B2B_SETTINGS_KEY, JSON.stringify(settings))
}, [settings, mounted])
useEffect(() => {
if (mounted) localStorage.setItem(B2B_SOURCES_KEY, JSON.stringify(sources))
}, [sources, mounted])
useEffect(() => {
if (mounted) localStorage.setItem(B2B_TOPICS_KEY, JSON.stringify(topics))
}, [topics, mounted])
useEffect(() => {
if (mounted && hits.length > 0) localStorage.setItem(B2B_HITS_KEY, JSON.stringify(hits))
}, [hits, mounted])
// Tenant operations
const updateTenant = useCallback((updates: Partial<B2BTenant>) => {
setTenant(prev => ({ ...prev, ...updates }))
}, [])
const updateSettings = useCallback((updates: Partial<B2BSettings>) => {
setSettings(prev => ({ ...prev, ...updates }))
}, [])
// Template operations
const selectTemplate = useCallback((templateId: string) => {
const template = availableTemplates.find(t => t.templateId === templateId)
if (template) {
setSelectedTemplate(template)
updateSettings({
selectedTemplateId: templateId,
selectedRegions: template.guidedConfig.regionSelector.default,
selectedLanguages: template.guidedConfig.languageSelector.default,
selectedPackages: template.guidedConfig.packageSelector.default,
noiseMode: template.guidedConfig.noiseMode.default,
notificationCadence: template.notificationPreset.cadence,
notificationTime: template.notificationPreset.timeLocal,
maxDigestItems: template.notificationPreset.maxItems
})
const newTopics: B2BTopic[] = template.topics.map((t, idx) => ({
...t,
id: `topic-${Date.now()}-${idx}`,
tenantId: tenant.id,
createdAt: new Date()
}))
setTopics(newTopics)
}
}, [availableTemplates, tenant.id, updateSettings])
// Source operations
const generateInboundEmail = useCallback(() => {
const token = Math.random().toString(36).substring(2, 10)
return `alerts+${tenant.id}-${token}@${tenant.inboundEmailDomain}`
}, [tenant.id, tenant.inboundEmailDomain])
const addSource = useCallback((source: Omit<AlertSource, 'id' | 'createdAt'>) => {
const newSource: AlertSource = {
...source,
id: `source-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
createdAt: new Date()
}
setSources(prev => [...prev, newSource])
}, [])
const removeSource = useCallback((id: string) => {
setSources(prev => prev.filter(s => s.id !== id))
}, [])
// Topic operations
const addTopic = useCallback((topic: Omit<B2BTopic, 'id' | 'createdAt'>) => {
const newTopic: B2BTopic = {
...topic,
id: `topic-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
createdAt: new Date()
}
setTopics(prev => [...prev, newTopic])
}, [])
const updateTopic = useCallback((id: string, updates: Partial<B2BTopic>) => {
setTopics(prev => prev.map(t => t.id === id ? { ...t, ...updates } : t))
}, [])
const removeTopic = useCallback((id: string) => {
setTopics(prev => prev.filter(t => t.id !== id))
}, [])
// Hit operations
const markAsRead = useCallback((id: string) => {
setHits(prev => prev.map(h => h.id === id ? { ...h, isRead: true } : h))
}, [])
const markAllAsRead = useCallback(() => {
setHits(prev => prev.map(h => ({ ...h, isRead: true })))
}, [])
const submitFeedback = useCallback((id: string, feedback: 'relevant' | 'irrelevant') => {
setHits(prev => prev.map(h => h.id === id ? { ...h, userFeedback: feedback } : h))
}, [])
const processEmailContent = useCallback((emailContent: string, emailSubject?: string): B2BHit => {
const newHit = processEmailToHit(
emailContent,
tenant.id,
topics[0]?.id || 'default',
emailSubject
)
setHits(prev => [newHit, ...prev])
return newHit
}, [tenant.id, topics])
// Digest
const getDigest = useCallback((maxItems: number = settings.maxDigestItems) => {
return hits
.filter(h => h.decisionLabel === 'relevant' || h.decisionLabel === 'needs_review')
.sort((a, b) => b.importanceScore - a.importanceScore)
.slice(0, maxItems)
}, [hits, settings.maxDigestItems])
// Computed values
const unreadCount = hits.filter(h => !h.isRead && h.decisionLabel !== 'irrelevant').length
const relevantCount = hits.filter(h => h.decisionLabel === 'relevant').length
const needsReviewCount = hits.filter(h => h.decisionLabel === 'needs_review').length
// SSR safety
if (!mounted) {
return (
<AlertsB2BContext.Provider
value={{
tenant: defaultTenant,
updateTenant: () => {},
settings: defaultB2BSettings,
updateSettings: () => {},
availableTemplates: [],
selectedTemplate: null,
selectTemplate: () => {},
sources: [],
addSource: () => {},
removeSource: () => {},
generateInboundEmail: () => '',
topics: [],
addTopic: () => {},
updateTopic: () => {},
removeTopic: () => {},
hits: [],
unreadCount: 0,
relevantCount: 0,
needsReviewCount: 0,
markAsRead: () => {},
markAllAsRead: () => {},
submitFeedback: () => {},
processEmailContent: () => ({} as B2BHit),
getDigest: () => [],
isLoading: false,
error: null
}}
>
{children}
</AlertsB2BContext.Provider>
)
}
return (
<AlertsB2BContext.Provider
value={{
tenant,
updateTenant,
settings,
updateSettings,
availableTemplates,
selectedTemplate,
selectTemplate,
sources,
addSource,
removeSource,
generateInboundEmail,
topics,
addTopic,
updateTopic,
removeTopic,
hits,
unreadCount,
relevantCount,
needsReviewCount,
markAsRead,
markAllAsRead,
submitFeedback,
processEmailContent,
getDigest,
isLoading,
error
}}
>
{children}
</AlertsB2BContext.Provider>
)
}
export function useAlertsB2B() {
const context = useContext(AlertsB2BContext)
if (!context) {
throw new Error('useAlertsB2B must be used within an AlertsB2BProvider')
}
return context
}