'use client' /** * Unified Inbox - User Frontend * * Main email interface for users with: * - Unified inbox view (all accounts) * - Email detail view with AI analysis * - Quick actions and response suggestions * * See: docs/klausur-modul/UNIFIED-INBOX-SPECIFICATION.md */ import { useState, useEffect, useCallback } from 'react' // API Base URL const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' // Types interface Email { id: string accountId: string accountEmail: string messageId: string subject: string senderName: string senderEmail: string recipients: string[] date: string bodyPreview: string bodyHtml?: string bodyText?: string isRead: boolean hasAttachments: boolean attachmentCount: number // AI Analysis senderType?: string category?: string priority?: string deadlines?: Deadline[] aiAnalyzed: boolean } interface Deadline { date: string description: string isFirm: boolean confidence: number } interface Account { id: string email: string displayName: string unreadCount: number } interface Folder { name: string icon: JSX.Element count?: number } export default function MailPage() { const [emails, setEmails] = useState([]) const [accounts, setAccounts] = useState([]) const [selectedEmail, setSelectedEmail] = useState(null) const [selectedAccount, setSelectedAccount] = useState('all') const [selectedFolder, setSelectedFolder] = useState('inbox') const [loading, setLoading] = useState(true) const [searchQuery, setSearchQuery] = useState('') const folders: Folder[] = [ { name: 'inbox', icon: ( ), }, { name: 'unread', icon: ( ), }, { name: 'tasks', icon: ( ), }, { name: 'sent', icon: ( ), }, ] const folderLabels: Record = { inbox: 'Posteingang', unread: 'Ungelesen', tasks: 'Aufgaben', sent: 'Gesendet', } const fetchEmails = useCallback(async () => { try { setLoading(true) const params = new URLSearchParams() if (selectedAccount !== 'all') { params.append('account_id', selectedAccount) } if (selectedFolder === 'unread') { params.append('unread_only', 'true') } if (searchQuery) { params.append('search', searchQuery) } const res = await fetch(`${API_BASE}/api/v1/mail/inbox?${params}`) if (res.ok) { const data = await res.json() setEmails(data.emails || []) } } catch (err) { console.error('Failed to fetch emails:', err) } finally { setLoading(false) } }, [selectedAccount, selectedFolder, searchQuery]) const fetchAccounts = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/v1/mail/accounts`) if (res.ok) { const data = await res.json() setAccounts(data.accounts || []) } } catch (err) { console.error('Failed to fetch accounts:', err) } }, []) useEffect(() => { fetchAccounts() }, [fetchAccounts]) useEffect(() => { fetchEmails() }, [fetchEmails]) const analyzeEmail = async (emailId: string) => { try { const res = await fetch(`${API_BASE}/api/v1/mail/analyze/${emailId}`, { method: 'POST', }) if (res.ok) { const data = await res.json() // Update the selected email with analysis results setSelectedEmail((prev) => prev ? { ...prev, ...data, aiAnalyzed: true } : null) // Update the email in the list setEmails((prev) => prev.map((e) => (e.id === emailId ? { ...e, ...data, aiAnalyzed: true } : e)) ) } } catch (err) { console.error('Failed to analyze email:', err) } } const markAsRead = async (emailId: string) => { try { await fetch(`${API_BASE}/api/v1/mail/inbox/${emailId}/read`, { method: 'POST', }) setEmails((prev) => prev.map((e) => (e.id === emailId ? { ...e, isRead: true } : e)) ) } catch (err) { console.error('Failed to mark as read:', err) } } const handleEmailClick = (email: Email) => { setSelectedEmail(email) if (!email.isRead) { markAsRead(email.id) } } const totalUnread = accounts.reduce((sum, acc) => sum + acc.unreadCount, 0) return (
{/* Header */}

Unified Inbox

{totalUnread > 0 && ( {totalUnread} ungelesen )}
{/* Search */}
setSearchQuery(e.target.value)} className="w-64 pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
{/* Compose Button */} Verfassen
{/* Main Content */}
{/* Sidebar */} {/* Email List */}
{loading ? (
) : emails.length === 0 ? (

Keine E-Mails gefunden

) : (
{emails.map((email) => ( handleEmailClick(email)} /> ))}
)}
{/* Email Detail / AI Panel */}
{selectedEmail ? ( analyzeEmail(selectedEmail.id)} /> ) : (

Wählen Sie eine E-Mail aus

Klicken Sie auf eine E-Mail, um sie zu lesen

)}
) } // ============================================================================ // Email List Item // ============================================================================ function EmailListItem({ email, isSelected, onClick }: { email: Email isSelected: boolean onClick: () => void }) { const priorityColors: Record = { urgent: 'bg-red-500', high: 'bg-orange-500', medium: 'bg-yellow-500', low: 'bg-green-500', } const senderTypeLabels: Record = { kultusministerium: 'MK', landesschulbehoerde: 'NLSchB', rlsb: 'RLSB', nibis: 'NiBiS', schulamt: 'Schulamt', } return ( ) } // ============================================================================ // Email Detail // ============================================================================ function EmailDetail({ email, onAnalyze }: { email: Email onAnalyze: () => void }) { const [showAIPanel, setShowAIPanel] = useState(true) const senderTypeLabels: Record = { kultusministerium: { label: 'Kultusministerium', color: 'bg-purple-100 text-purple-800' }, landesschulbehoerde: { label: 'Landesschulbehörde', color: 'bg-blue-100 text-blue-800' }, rlsb: { label: 'RLSB', color: 'bg-indigo-100 text-indigo-800' }, nibis: { label: 'NiBiS', color: 'bg-cyan-100 text-cyan-800' }, schulamt: { label: 'Schulamt', color: 'bg-teal-100 text-teal-800' }, elternvertreter: { label: 'Elternvertreter', color: 'bg-green-100 text-green-800' }, privatperson: { label: 'Privatperson', color: 'bg-slate-100 text-slate-800' }, } const categoryLabels: Record = { dienstlich: 'Dienstlich', personal: 'Personal', finanzen: 'Finanzen', eltern: 'Eltern', schueler: 'Schüler', fortbildung: 'Fortbildung', veranstaltung: 'Veranstaltung', sicherheit: 'Sicherheit', newsletter: 'Newsletter', } return (
{/* Email Content */}
{/* Header */}

{email.subject || '(Kein Betreff)'}

{(email.senderName || email.senderEmail)[0].toUpperCase()}

{email.senderName || email.senderEmail}

{email.senderEmail}

{new Date(email.date).toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', })}

An: {email.recipients.join(', ')}

{/* Action Buttons */}
{!email.aiAnalyzed && ( )}
{/* Body */}
{email.bodyHtml ? (
) : (
              {email.bodyText || email.bodyPreview}
            
)} {/* Attachments */} {email.hasAttachments && (

Anhänge ({email.attachmentCount})

Anhang herunterladen
)}
{/* AI Panel */} {showAIPanel && (

KI-Analyse

{email.aiAnalyzed ? (
{/* Sender Classification */} {email.senderType && senderTypeLabels[email.senderType] && (

Absender-Typ

{senderTypeLabels[email.senderType].label}
)} {/* Category */} {email.category && (

Kategorie

{categoryLabels[email.category] || email.category}
)} {/* Deadlines */} {email.deadlines && email.deadlines.length > 0 && (

Erkannte Fristen

{email.deadlines.map((deadline, idx) => (
{new Date(deadline.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', })} {deadline.isFirm && ( verbindlich )}

{deadline.description}

Konfidenz: {Math.round(deadline.confidence * 100)}%

))}
)} {/* Quick Actions */}

Schnellaktionen

) : (

Diese E-Mail wurde noch nicht analysiert.

)}
)}
) }