Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
356 lines
13 KiB
TypeScript
356 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { EmailAccount } from '../types'
|
|
import { API_BASE } from '../types'
|
|
|
|
interface AccountsTabProps {
|
|
accounts: EmailAccount[]
|
|
loading: boolean
|
|
onRefresh: () => void
|
|
}
|
|
|
|
const statusColors: Record<EmailAccount['status'], string> = {
|
|
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: Record<EmailAccount['status'], string> = {
|
|
active: 'Aktiv',
|
|
inactive: 'Inaktiv',
|
|
error: 'Fehler',
|
|
syncing: 'Synchronisiert...',
|
|
}
|
|
|
|
export function AccountsTab({ accounts, loading, onRefresh }: AccountsTabProps) {
|
|
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 {
|
|
alert('Verbindungsfehler')
|
|
}
|
|
}
|
|
|
|
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 ? (
|
|
<EmptyAccountsState />
|
|
) : (
|
|
accounts.map((account) => (
|
|
<AccountCard
|
|
key={account.id}
|
|
account={account}
|
|
onTestConnection={testConnection}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Add Account Modal */}
|
|
{showAddModal && (
|
|
<AddAccountModal
|
|
onClose={() => setShowAddModal(false)}
|
|
onSuccess={() => { setShowAddModal(false); onRefresh() }}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function EmptyAccountsState() {
|
|
return (
|
|
<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>
|
|
)
|
|
}
|
|
|
|
function AccountCard({
|
|
account,
|
|
onTestConnection
|
|
}: {
|
|
account: EmailAccount
|
|
onTestConnection: (id: string) => void
|
|
}) {
|
|
return (
|
|
<div 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={() => onTestConnection(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>
|
|
)
|
|
}
|
|
|
|
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 {
|
|
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>
|
|
)
|
|
}
|