refactor: Unified Inbox aus Core entfernt (nach Lehrer migriert)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 29s
CI / test-python-voice (push) Successful in 27s
CI / test-bqas (push) Successful in 29s
CI / nodejs-lint (push) Has been skipped
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 29s
CI / test-python-voice (push) Successful in 27s
CI / test-bqas (push) Successful in 29s
CI / nodejs-lint (push) Has been skipped
- Mail-Seite, API-Route, Kommunikation-Kategorie entfernt - Screen-Flow: Mail-Node und Kommunikation-Legende entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,945 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified Inbox Mail Admin Page
|
|
||||||
* Migrated from website/admin/mail to admin-v2/communication/mail
|
|
||||||
*
|
|
||||||
* Admin interface for managing email accounts, viewing system status,
|
|
||||||
* and configuring AI analysis settings.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
||||||
|
|
||||||
// API Base URL for backend operations (accounts, sync, etc.)
|
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://macmini:8086'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
interface EmailAccount {
|
|
||||||
id: string
|
|
||||||
email: string
|
|
||||||
displayName: string
|
|
||||||
imapHost: string
|
|
||||||
imapPort: number
|
|
||||||
smtpHost: string
|
|
||||||
smtpPort: number
|
|
||||||
status: 'active' | 'inactive' | 'error' | 'syncing'
|
|
||||||
lastSync: string | null
|
|
||||||
emailCount: number
|
|
||||||
unreadCount: number
|
|
||||||
createdAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MailStats {
|
|
||||||
totalAccounts: number
|
|
||||||
activeAccounts: number
|
|
||||||
totalEmails: number
|
|
||||||
unreadEmails: number
|
|
||||||
totalTasks: number
|
|
||||||
pendingTasks: number
|
|
||||||
overdueTasks: number
|
|
||||||
aiAnalyzedCount: number
|
|
||||||
lastSyncTime: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SyncStatus {
|
|
||||||
running: boolean
|
|
||||||
accountsInProgress: string[]
|
|
||||||
lastCompleted: string | null
|
|
||||||
errors: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab definitions
|
|
||||||
type TabId = 'overview' | 'accounts' | 'ai-settings' | 'templates' | 'logs'
|
|
||||||
|
|
||||||
const tabs: { id: TabId; name: string }[] = [
|
|
||||||
{ id: 'overview', name: 'Uebersicht' },
|
|
||||||
{ id: 'accounts', name: 'Konten' },
|
|
||||||
{ id: 'ai-settings', name: 'KI-Einstellungen' },
|
|
||||||
{ id: 'templates', name: 'Vorlagen' },
|
|
||||||
{ id: 'logs', name: 'Audit-Log' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Main Component
|
|
||||||
export default function MailAdminPage() {
|
|
||||||
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
||||||
const [stats, setStats] = useState<MailStats | null>(null)
|
|
||||||
const [accounts, setAccounts] = useState<EmailAccount[]>([])
|
|
||||||
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
// Fetch stats via our proxy API (avoids CORS/mixed-content issues)
|
|
||||||
const response = await fetch('/api/admin/mail')
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
setStats(data.stats)
|
|
||||||
setAccounts(data.accounts)
|
|
||||||
setSyncStatus(data.syncStatus)
|
|
||||||
setError(null)
|
|
||||||
} else {
|
|
||||||
const errorData = await response.json().catch(() => ({}))
|
|
||||||
throw new Error(errorData.details || `API returned ${response.status}`)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch mail data:', err)
|
|
||||||
setError('Verbindung zum Mail-Service (Mailpit) fehlgeschlagen. Laeuft Mailpit auf Port 8025?')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData()
|
|
||||||
|
|
||||||
// Refresh every 10 seconds if syncing
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (syncStatus?.running) {
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
}, 10000)
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [fetchData, syncStatus?.running])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Page Purpose */}
|
|
||||||
<PagePurpose
|
|
||||||
title="Unified Inbox"
|
|
||||||
purpose="Verwalten Sie E-Mail-Konten, synchronisieren Sie Postfaecher und konfigurieren Sie die KI-gestuetzte E-Mail-Analyse fuer automatische Kategorisierung und Aufgabenerkennung."
|
|
||||||
audience={['Admins', 'Schulleitung']}
|
|
||||||
architecture={{
|
|
||||||
services: ['Mailpit (Dev Mail Catcher)', 'IMAP/SMTP Server (Prod)'],
|
|
||||||
databases: ['PostgreSQL', 'Vault (Credentials)'],
|
|
||||||
}}
|
|
||||||
relatedPages={[
|
|
||||||
{ name: 'Mail Wizard', href: '/communication/mail/wizard', description: 'Interaktives Setup und Testing' },
|
|
||||||
]}
|
|
||||||
collapsible={true}
|
|
||||||
defaultCollapsed={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Quick Link to Wizard */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<Link
|
|
||||||
href="/communication/mail/wizard"
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
||||||
</svg>
|
|
||||||
Mail Wizard starten
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Banner */}
|
|
||||||
{error && (
|
|
||||||
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3">
|
|
||||||
<svg className="w-5 h-5 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-red-700">{error}</span>
|
|
||||||
<button onClick={fetchData} className="ml-auto text-red-600 hover:text-red-800 text-sm font-medium">
|
|
||||||
Erneut versuchen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
|
||||||
<div className="border-b border-slate-200 mb-6">
|
|
||||||
<nav className="-mb-px flex space-x-8">
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<button
|
|
||||||
key={tab.id}
|
|
||||||
onClick={() => setActiveTab(tab.id)}
|
|
||||||
className={`
|
|
||||||
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
|
||||||
${activeTab === tab.id
|
|
||||||
? 'border-blue-500 text-blue-600'
|
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{tab.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
{activeTab === 'overview' && (
|
|
||||||
<OverviewTab
|
|
||||||
stats={stats}
|
|
||||||
syncStatus={syncStatus}
|
|
||||||
loading={loading}
|
|
||||||
onRefresh={fetchData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTab === 'accounts' && (
|
|
||||||
<AccountsTab
|
|
||||||
accounts={accounts}
|
|
||||||
loading={loading}
|
|
||||||
onRefresh={fetchData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTab === 'ai-settings' && (
|
|
||||||
<AISettingsTab />
|
|
||||||
)}
|
|
||||||
{activeTab === 'templates' && (
|
|
||||||
<TemplatesTab />
|
|
||||||
)}
|
|
||||||
{activeTab === 'logs' && (
|
|
||||||
<AuditLogTab />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Overview Tab
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function OverviewTab({
|
|
||||||
stats,
|
|
||||||
syncStatus,
|
|
||||||
loading,
|
|
||||||
onRefresh
|
|
||||||
}: {
|
|
||||||
stats: MailStats | null
|
|
||||||
syncStatus: SyncStatus | null
|
|
||||||
loading: boolean
|
|
||||||
onRefresh: () => void
|
|
||||||
}) {
|
|
||||||
const triggerSync = async () => {
|
|
||||||
try {
|
|
||||||
await fetch(`${API_BASE}/api/v1/mail/sync/all`, {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
onRefresh()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to trigger sync:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900">System-Uebersicht</h2>
|
|
||||||
<p className="text-sm text-slate-500">Status aller E-Mail-Konten und Aufgaben</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={onRefresh}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
Aktualisieren
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={triggerSync}
|
|
||||||
disabled={syncStatus?.running}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{syncStatus?.running ? 'Synchronisiert...' : 'Alle synchronisieren'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Loading State */}
|
|
||||||
{loading && (
|
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stats Grid */}
|
|
||||||
{!loading && stats && (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<StatCard
|
|
||||||
title="E-Mail-Konten"
|
|
||||||
value={stats.totalAccounts}
|
|
||||||
subtitle={`${stats.activeAccounts} aktiv`}
|
|
||||||
color="blue"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="E-Mails gesamt"
|
|
||||||
value={stats.totalEmails}
|
|
||||||
subtitle={`${stats.unreadEmails} ungelesen`}
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Aufgaben"
|
|
||||||
value={stats.totalTasks}
|
|
||||||
subtitle={`${stats.pendingTasks} offen`}
|
|
||||||
color="yellow"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Ueberfaellig"
|
|
||||||
value={stats.overdueTasks}
|
|
||||||
color={stats.overdueTasks > 0 ? 'red' : 'green'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sync Status */}
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
|
||||||
<h3 className="text-sm font-medium text-slate-700 mb-4">Synchronisierung</h3>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{syncStatus?.running ? (
|
|
||||||
<>
|
|
||||||
<div className="w-3 h-3 bg-yellow-500 rounded-full animate-pulse"></div>
|
|
||||||
<span className="text-slate-600">
|
|
||||||
Synchronisiere {syncStatus.accountsInProgress.length} Konto(en)...
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
|
||||||
<span className="text-slate-600">Bereit</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{stats.lastSyncTime && (
|
|
||||||
<span className="text-sm text-slate-500 ml-auto">
|
|
||||||
Letzte Sync: {new Date(stats.lastSyncTime).toLocaleString('de-DE')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{syncStatus?.errors && syncStatus.errors.length > 0 && (
|
|
||||||
<div className="mt-4 p-4 bg-red-50 rounded-lg">
|
|
||||||
<h4 className="text-sm font-medium text-red-800 mb-2">Fehler</h4>
|
|
||||||
<ul className="text-sm text-red-700 space-y-1">
|
|
||||||
{syncStatus.errors.slice(0, 3).map((error, i) => (
|
|
||||||
<li key={i}>{error}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* AI Stats */}
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
|
||||||
<h3 className="text-sm font-medium text-slate-700 mb-4">KI-Analyse</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Analysiert</p>
|
|
||||||
<p className="text-2xl font-bold text-slate-900">{stats.aiAnalyzedCount}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Analyse-Rate</p>
|
|
||||||
<p className="text-2xl font-bold text-slate-900">
|
|
||||||
{stats.totalEmails > 0
|
|
||||||
? `${Math.round((stats.aiAnalyzedCount / stats.totalEmails) * 100)}%`
|
|
||||||
: '0%'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatCard({
|
|
||||||
title,
|
|
||||||
value,
|
|
||||||
subtitle,
|
|
||||||
color = 'blue'
|
|
||||||
}: {
|
|
||||||
title: string
|
|
||||||
value: number
|
|
||||||
subtitle?: string
|
|
||||||
color?: 'blue' | 'green' | 'yellow' | 'red'
|
|
||||||
}) {
|
|
||||||
const colorClasses = {
|
|
||||||
blue: 'text-blue-600',
|
|
||||||
green: 'text-green-600',
|
|
||||||
yellow: 'text-yellow-600',
|
|
||||||
red: 'text-red-600',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
|
||||||
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">{title}</p>
|
|
||||||
<p className={`text-3xl font-bold ${colorClasses[color]}`}>{value.toLocaleString()}</p>
|
|
||||||
{subtitle && <p className="text-sm text-slate-500 mt-1">{subtitle}</p>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Accounts Tab
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function AccountsTab({
|
|
||||||
accounts,
|
|
||||||
loading,
|
|
||||||
onRefresh
|
|
||||||
}: {
|
|
||||||
accounts: EmailAccount[]
|
|
||||||
loading: boolean
|
|
||||||
onRefresh: () => void
|
|
||||||
}) {
|
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
|
||||||
|
|
||||||
const testConnection = async (accountId: string) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/api/v1/mail/accounts/${accountId}/test`, {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
alert('Verbindung erfolgreich!')
|
|
||||||
} else {
|
|
||||||
alert('Verbindungsfehler')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert('Verbindungsfehler')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusColors = {
|
|
||||||
active: 'bg-green-100 text-green-800',
|
|
||||||
inactive: 'bg-gray-100 text-gray-800',
|
|
||||||
error: 'bg-red-100 text-red-800',
|
|
||||||
syncing: 'bg-yellow-100 text-yellow-800',
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusLabels = {
|
|
||||||
active: 'Aktiv',
|
|
||||||
inactive: 'Inaktiv',
|
|
||||||
error: 'Fehler',
|
|
||||||
syncing: 'Synchronisiert...',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900">E-Mail-Konten</h2>
|
|
||||||
<p className="text-sm text-slate-500">Verwalten Sie die verbundenen E-Mail-Konten</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAddModal(true)}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
Konto hinzufuegen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Loading State */}
|
|
||||||
{loading && (
|
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Accounts Grid */}
|
|
||||||
{!loading && (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{accounts.length === 0 ? (
|
|
||||||
<div className="bg-slate-50 rounded-lg p-8 text-center">
|
|
||||||
<svg className="w-12 h-12 text-slate-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
<h3 className="text-lg font-medium text-slate-900 mb-2">Keine E-Mail-Konten</h3>
|
|
||||||
<p className="text-slate-500 mb-4">Fuegen Sie Ihr erstes E-Mail-Konto hinzu.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
accounts.map((account) => (
|
|
||||||
<div
|
|
||||||
key={account.id}
|
|
||||||
className="bg-white rounded-lg border border-slate-200 p-6 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
|
||||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-slate-900">
|
|
||||||
{account.displayName || account.email}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-slate-500">{account.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${statusColors[account.status]}`}>
|
|
||||||
{statusLabels[account.status]}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => testConnection(account.id)}
|
|
||||||
className="p-2 text-slate-400 hover:text-slate-600"
|
|
||||||
title="Verbindung testen"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500 uppercase tracking-wider">E-Mails</p>
|
|
||||||
<p className="text-lg font-semibold text-slate-900">{account.emailCount}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Ungelesen</p>
|
|
||||||
<p className="text-lg font-semibold text-slate-900">{account.unreadCount}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500 uppercase tracking-wider">IMAP</p>
|
|
||||||
<p className="text-sm font-mono text-slate-700">{account.imapHost}:{account.imapPort}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Letzte Sync</p>
|
|
||||||
<p className="text-sm text-slate-700">
|
|
||||||
{account.lastSync
|
|
||||||
? new Date(account.lastSync).toLocaleString('de-DE')
|
|
||||||
: 'Nie'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add Account Modal */}
|
|
||||||
{showAddModal && (
|
|
||||||
<AddAccountModal onClose={() => setShowAddModal(false)} onSuccess={() => { setShowAddModal(false); onRefresh(); }} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddAccountModal({
|
|
||||||
onClose,
|
|
||||||
onSuccess
|
|
||||||
}: {
|
|
||||||
onClose: () => void
|
|
||||||
onSuccess: () => void
|
|
||||||
}) {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
email: '',
|
|
||||||
displayName: '',
|
|
||||||
imapHost: '',
|
|
||||||
imapPort: 993,
|
|
||||||
smtpHost: '',
|
|
||||||
smtpPort: 587,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
})
|
|
||||||
const [submitting, setSubmitting] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setSubmitting(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/api/v1/mail/accounts`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: formData.email,
|
|
||||||
display_name: formData.displayName,
|
|
||||||
imap_host: formData.imapHost,
|
|
||||||
imap_port: formData.imapPort,
|
|
||||||
smtp_host: formData.smtpHost,
|
|
||||||
smtp_port: formData.smtpPort,
|
|
||||||
username: formData.username,
|
|
||||||
password: formData.password,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
onSuccess()
|
|
||||||
} else {
|
|
||||||
const data = await res.json()
|
|
||||||
setError(data.detail || 'Fehler beim Hinzufuegen des Kontos')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Netzwerkfehler')
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="p-6 border-b border-slate-200">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900">E-Mail-Konto hinzufuegen</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">E-Mail-Adresse</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="schulleitung@grundschule-xy.de"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Anzeigename</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.displayName}
|
|
||||||
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Schulleitung"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">IMAP Server</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.imapHost}
|
|
||||||
onChange={(e) => setFormData({ ...formData, imapHost: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="imap.example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">IMAP Port</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
value={formData.imapPort}
|
|
||||||
onChange={(e) => setFormData({ ...formData, imapPort: parseInt(e.target.value) })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">SMTP Server</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.smtpHost}
|
|
||||||
onChange={(e) => setFormData({ ...formData, smtpHost: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="smtp.example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">SMTP Port</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
value={formData.smtpPort}
|
|
||||||
onChange={(e) => setFormData({ ...formData, smtpPort: parseInt(e.target.value) })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Benutzername</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.username}
|
|
||||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Passwort</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
|
||||||
Das Passwort wird verschluesselt in Vault gespeichert.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t border-slate-200">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-lg"
|
|
||||||
>
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={submitting}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{submitting ? 'Speichern...' : 'Konto hinzufuegen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// AI Settings Tab
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function AISettingsTab() {
|
|
||||||
const [settings, setSettings] = useState({
|
|
||||||
autoAnalyze: true,
|
|
||||||
autoCreateTasks: true,
|
|
||||||
analysisModel: 'breakpilot-teacher-8b',
|
|
||||||
confidenceThreshold: 0.7,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900">KI-Einstellungen</h2>
|
|
||||||
<p className="text-sm text-slate-500">Konfigurieren Sie die automatische E-Mail-Analyse</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-6 space-y-6">
|
|
||||||
{/* Auto-Analyze */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-slate-900">Automatische Analyse</h3>
|
|
||||||
<p className="text-sm text-slate-500">E-Mails automatisch beim Empfang analysieren</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setSettings({ ...settings, autoAnalyze: !settings.autoAnalyze })}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
||||||
settings.autoAnalyze ? 'bg-blue-600' : 'bg-slate-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
||||||
settings.autoAnalyze ? 'translate-x-6' : 'translate-x-1'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Auto-Create Tasks */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-slate-900">Aufgaben automatisch erstellen</h3>
|
|
||||||
<p className="text-sm text-slate-500">Erkannte Fristen als Aufgaben anlegen</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setSettings({ ...settings, autoCreateTasks: !settings.autoCreateTasks })}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
||||||
settings.autoCreateTasks ? 'bg-blue-600' : 'bg-slate-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
||||||
settings.autoCreateTasks ? 'translate-x-6' : 'translate-x-1'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Model Selection */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Analyse-Modell</label>
|
|
||||||
<select
|
|
||||||
value={settings.analysisModel}
|
|
||||||
onChange={(e) => setSettings({ ...settings, analysisModel: e.target.value })}
|
|
||||||
className="w-full md:w-64 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="breakpilot-teacher-8b">BreakPilot Teacher 8B (schnell)</option>
|
|
||||||
<option value="breakpilot-teacher-70b">BreakPilot Teacher 70B (genau)</option>
|
|
||||||
<option value="llama-3.1-8b-instruct">Llama 3.1 8B Instruct</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Confidence Threshold */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
||||||
Konfidenz-Schwelle: {Math.round(settings.confidenceThreshold * 100)}%
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.5"
|
|
||||||
max="0.95"
|
|
||||||
step="0.05"
|
|
||||||
value={settings.confidenceThreshold}
|
|
||||||
onChange={(e) => setSettings({ ...settings, confidenceThreshold: parseFloat(e.target.value) })}
|
|
||||||
className="w-full md:w-64"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
|
||||||
Mindest-Konfidenz fuer automatische Aufgabenerstellung
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sender Classification */}
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
|
||||||
<h3 className="text-sm font-medium text-slate-700 mb-4">Bekannte Absender (Niedersachsen)</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
||||||
{[
|
|
||||||
{ domain: '@mk.niedersachsen.de', type: 'Kultusministerium', priority: 'Hoch' },
|
|
||||||
{ domain: '@rlsb.de', type: 'RLSB', priority: 'Hoch' },
|
|
||||||
{ domain: '@landesschulbehoerde-nds.de', type: 'Landesschulbehoerde', priority: 'Hoch' },
|
|
||||||
{ domain: '@nibis.de', type: 'NiBiS', priority: 'Mittel' },
|
|
||||||
{ domain: '@schultraeger.de', type: 'Schultraeger', priority: 'Mittel' },
|
|
||||||
].map((sender) => (
|
|
||||||
<div key={sender.domain} className="p-3 bg-slate-50 rounded-lg">
|
|
||||||
<p className="text-sm font-mono text-slate-700">{sender.domain}</p>
|
|
||||||
<p className="text-xs text-slate-500">{sender.type}</p>
|
|
||||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
||||||
sender.priority === 'Hoch' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700'
|
|
||||||
}`}>
|
|
||||||
{sender.priority}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Templates Tab
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function TemplatesTab() {
|
|
||||||
const [templates] = useState([
|
|
||||||
{ id: '1', name: 'Eingangsbestaetigung', category: 'Standard', usageCount: 45 },
|
|
||||||
{ id: '2', name: 'Terminbestaetigung', category: 'Termine', usageCount: 23 },
|
|
||||||
{ id: '3', name: 'Elternbrief-Vorlage', category: 'Eltern', usageCount: 67 },
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900">E-Mail-Vorlagen</h2>
|
|
||||||
<p className="text-sm text-slate-500">Verwalten Sie Antwort-Templates</p>
|
|
||||||
</div>
|
|
||||||
<button className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 flex items-center gap-2">
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
Vorlage erstellen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-slate-200">
|
|
||||||
<thead className="bg-slate-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Name</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Kategorie</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Verwendet</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-200">
|
|
||||||
{templates.map((template) => (
|
|
||||||
<tr key={template.id} className="hover:bg-slate-50">
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-slate-900">{template.name}</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<span className="px-2 py-1 bg-slate-100 text-slate-700 text-xs rounded">{template.category}</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-slate-500">{template.usageCount}x</td>
|
|
||||||
<td className="px-6 py-4 text-right">
|
|
||||||
<button className="text-blue-600 hover:text-blue-800 text-sm font-medium">Bearbeiten</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Audit Log Tab
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function AuditLogTab() {
|
|
||||||
const [logs] = useState([
|
|
||||||
{ id: '1', action: 'account_created', user: 'admin@breakpilot.de', timestamp: new Date().toISOString(), details: 'Konto schulleitung@example.de hinzugefuegt' },
|
|
||||||
{ id: '2', action: 'email_analyzed', user: 'system', timestamp: new Date(Date.now() - 3600000).toISOString(), details: '5 E-Mails analysiert' },
|
|
||||||
{ id: '3', action: 'task_created', user: 'system', timestamp: new Date(Date.now() - 7200000).toISOString(), details: 'Aufgabe aus Fristenerkennung erstellt' },
|
|
||||||
])
|
|
||||||
|
|
||||||
const actionLabels: Record<string, string> = {
|
|
||||||
account_created: 'Konto erstellt',
|
|
||||||
email_analyzed: 'E-Mail analysiert',
|
|
||||||
task_created: 'Aufgabe erstellt',
|
|
||||||
sync_completed: 'Sync abgeschlossen',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900">Audit-Log</h2>
|
|
||||||
<p className="text-sm text-slate-500">Alle Aktionen im Mail-System</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-slate-200">
|
|
||||||
<thead className="bg-slate-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Zeit</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Aktion</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Benutzer</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Details</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-200">
|
|
||||||
{logs.map((log) => (
|
|
||||||
<tr key={log.id} className="hover:bg-slate-50">
|
|
||||||
<td className="px-6 py-4 text-sm text-slate-500">
|
|
||||||
{new Date(log.timestamp).toLocaleString('de-DE')}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<span className="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded font-medium">
|
|
||||||
{actionLabels[log.action] || log.action}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-slate-700">{log.user}</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-slate-500">{log.details}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -13,10 +13,9 @@ import ReactFlow, {
|
|||||||
} from 'reactflow'
|
} from 'reactflow'
|
||||||
import 'reactflow/dist/style.css'
|
import 'reactflow/dist/style.css'
|
||||||
|
|
||||||
type CategoryFilter = 'all' | 'communication' | 'infrastructure' | 'development' | 'meta'
|
type CategoryFilter = 'all' | 'infrastructure' | 'development' | 'meta'
|
||||||
|
|
||||||
const categoryColors: Record<string, string> = {
|
const categoryColors: Record<string, string> = {
|
||||||
communication: '#22c55e',
|
|
||||||
infrastructure: '#f97316',
|
infrastructure: '#f97316',
|
||||||
development: '#64748b',
|
development: '#64748b',
|
||||||
meta: '#0ea5e9',
|
meta: '#0ea5e9',
|
||||||
@@ -27,9 +26,6 @@ const initialNodes: Node[] = [
|
|||||||
{ id: 'role-select', position: { x: 400, y: 0 }, data: { label: 'Rollenauswahl', category: 'meta' }, style: { background: '#e0f2fe', border: '2px solid #0ea5e9', borderRadius: '12px', padding: '10px 16px' } },
|
{ id: 'role-select', position: { x: 400, y: 0 }, data: { label: 'Rollenauswahl', category: 'meta' }, style: { background: '#e0f2fe', border: '2px solid #0ea5e9', borderRadius: '12px', padding: '10px 16px' } },
|
||||||
{ id: 'dashboard', position: { x: 400, y: 100 }, data: { label: 'Dashboard', category: 'meta' }, style: { background: '#e0f2fe', border: '2px solid #0ea5e9', borderRadius: '12px', padding: '10px 16px' } },
|
{ id: 'dashboard', position: { x: 400, y: 100 }, data: { label: 'Dashboard', category: 'meta' }, style: { background: '#e0f2fe', border: '2px solid #0ea5e9', borderRadius: '12px', padding: '10px 16px' } },
|
||||||
|
|
||||||
// Communication (Green)
|
|
||||||
{ id: 'mail', position: { x: 50, y: 250 }, data: { label: 'Unified Inbox', category: 'communication' }, style: { background: '#dcfce7', border: '2px solid #22c55e', borderRadius: '12px', padding: '10px 16px' } },
|
|
||||||
|
|
||||||
// Infrastructure (Orange)
|
// Infrastructure (Orange)
|
||||||
{ id: 'gpu', position: { x: 300, y: 250 }, data: { label: 'GPU Infrastruktur', category: 'infrastructure' }, style: { background: '#ffedd5', border: '2px solid #f97316', borderRadius: '12px', padding: '10px 16px' } },
|
{ id: 'gpu', position: { x: 300, y: 250 }, data: { label: 'GPU Infrastruktur', category: 'infrastructure' }, style: { background: '#ffedd5', border: '2px solid #f97316', borderRadius: '12px', padding: '10px 16px' } },
|
||||||
{ id: 'middleware', position: { x: 300, y: 350 }, data: { label: 'Middleware', category: 'infrastructure' }, style: { background: '#ffedd5', border: '2px solid #f97316', borderRadius: '12px', padding: '10px 16px' } },
|
{ id: 'middleware', position: { x: 300, y: 350 }, data: { label: 'Middleware', category: 'infrastructure' }, style: { background: '#ffedd5', border: '2px solid #f97316', borderRadius: '12px', padding: '10px 16px' } },
|
||||||
@@ -49,7 +45,6 @@ const initialEdges: Edge[] = [
|
|||||||
{ id: 'e-role-dash', source: 'role-select', target: 'dashboard', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#0ea5e9' } },
|
{ id: 'e-role-dash', source: 'role-select', target: 'dashboard', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#0ea5e9' } },
|
||||||
|
|
||||||
// Dashboard to categories
|
// Dashboard to categories
|
||||||
{ id: 'e-dash-mail', source: 'dashboard', target: 'mail', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#22c55e' } },
|
|
||||||
{ id: 'e-dash-gpu', source: 'dashboard', target: 'gpu', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#f97316' } },
|
{ id: 'e-dash-gpu', source: 'dashboard', target: 'gpu', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#f97316' } },
|
||||||
{ id: 'e-dash-cicd', source: 'dashboard', target: 'ci-cd', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#f97316' } },
|
{ id: 'e-dash-cicd', source: 'dashboard', target: 'ci-cd', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#f97316' } },
|
||||||
{ id: 'e-dash-docs', source: 'dashboard', target: 'docs', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#64748b' } },
|
{ id: 'e-dash-docs', source: 'dashboard', target: 'docs', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: '#64748b' } },
|
||||||
@@ -83,7 +78,6 @@ export default function ScreenFlowPage() {
|
|||||||
|
|
||||||
const filters: { id: CategoryFilter; label: string; color: string }[] = [
|
const filters: { id: CategoryFilter; label: string; color: string }[] = [
|
||||||
{ id: 'all', label: 'Alle', color: '#0ea5e9' },
|
{ id: 'all', label: 'Alle', color: '#0ea5e9' },
|
||||||
{ id: 'communication', label: 'Kommunikation', color: '#22c55e' },
|
|
||||||
{ id: 'infrastructure', label: 'Infrastruktur', color: '#f97316' },
|
{ id: 'infrastructure', label: 'Infrastruktur', color: '#f97316' },
|
||||||
{ id: 'development', label: 'Entwicklung', color: '#64748b' },
|
{ id: 'development', label: 'Entwicklung', color: '#64748b' },
|
||||||
]
|
]
|
||||||
@@ -149,10 +143,6 @@ export default function ScreenFlowPage() {
|
|||||||
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div className="mt-4 flex items-center gap-6 text-sm text-slate-500">
|
<div className="mt-4 flex items-center gap-6 text-sm text-slate-500">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 rounded bg-green-100 border-2 border-green-500" />
|
|
||||||
<span>Kommunikation (4)</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 rounded bg-orange-100 border-2 border-orange-500" />
|
<div className="w-4 h-4 rounded bg-orange-100 border-2 border-orange-500" />
|
||||||
<span>Infrastruktur (6)</span>
|
<span>Infrastruktur (6)</span>
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server-side proxy for Mailpit API
|
|
||||||
* Avoids CORS and mixed-content issues by fetching from server
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Use internal Docker hostname when running in container
|
|
||||||
const getMailpitHost = (): string => {
|
|
||||||
return process.env.BACKEND_URL ? 'mailpit' : 'localhost'
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const host = getMailpitHost()
|
|
||||||
const mailpitUrl = `http://${host}:8025/api/v1/info`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(mailpitUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Mailpit API error', status: response.status },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// Transform Mailpit response to our expected format
|
|
||||||
return NextResponse.json({
|
|
||||||
stats: {
|
|
||||||
totalAccounts: 1,
|
|
||||||
activeAccounts: 1,
|
|
||||||
totalEmails: data.Messages || 0,
|
|
||||||
unreadEmails: data.Unread || 0,
|
|
||||||
totalTasks: 0,
|
|
||||||
pendingTasks: 0,
|
|
||||||
overdueTasks: 0,
|
|
||||||
aiAnalyzedCount: 0,
|
|
||||||
lastSyncTime: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
accounts: [{
|
|
||||||
id: 'mailpit-dev',
|
|
||||||
email: 'dev@mailpit.local',
|
|
||||||
displayName: 'Mailpit (Development)',
|
|
||||||
imapHost: 'mailpit',
|
|
||||||
imapPort: 1143,
|
|
||||||
smtpHost: 'mailpit',
|
|
||||||
smtpPort: 1025,
|
|
||||||
status: 'active' as const,
|
|
||||||
lastSync: new Date().toISOString(),
|
|
||||||
emailCount: data.Messages || 0,
|
|
||||||
unreadCount: data.Unread || 0,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
}],
|
|
||||||
syncStatus: {
|
|
||||||
running: false,
|
|
||||||
accountsInProgress: [],
|
|
||||||
lastCompleted: new Date().toISOString(),
|
|
||||||
errors: [],
|
|
||||||
},
|
|
||||||
mailpitInfo: {
|
|
||||||
version: data.Version,
|
|
||||||
databaseSize: data.DatabaseSize,
|
|
||||||
uptime: data.RuntimeStats?.Uptime,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch from Mailpit:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: 'Failed to connect to Mailpit',
|
|
||||||
details: error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
},
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* 3 Categories: Communication, Infrastructure, Development
|
* 3 Categories: Communication, Infrastructure, Development
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type CategoryId = 'communication' | 'infrastructure' | 'development'
|
export type CategoryId = 'infrastructure' | 'development'
|
||||||
|
|
||||||
export interface NavModule {
|
export interface NavModule {
|
||||||
id: string
|
id: string
|
||||||
@@ -27,27 +27,6 @@ export interface NavCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const navigation: NavCategory[] = [
|
export const navigation: NavCategory[] = [
|
||||||
// =========================================================================
|
|
||||||
// Kommunikation & Alerts (Green)
|
|
||||||
// =========================================================================
|
|
||||||
{
|
|
||||||
id: 'communication',
|
|
||||||
name: 'Kommunikation',
|
|
||||||
icon: 'message-circle',
|
|
||||||
color: '#22c55e',
|
|
||||||
colorClass: 'communication',
|
|
||||||
description: 'E-Mail Management',
|
|
||||||
modules: [
|
|
||||||
{
|
|
||||||
id: 'mail',
|
|
||||||
name: 'Unified Inbox',
|
|
||||||
href: '/communication/mail',
|
|
||||||
description: 'E-Mail-Konten & KI-Analyse',
|
|
||||||
purpose: 'E-Mail-Konten verwalten und KI-Kategorisierung nutzen.',
|
|
||||||
audience: ['Support', 'Admins'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Infrastruktur & DevOps (Orange)
|
// Infrastruktur & DevOps (Orange)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user