website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
7.3 KiB
TypeScript
169 lines
7.3 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Arbeitsvorrat (Task Manager) - User Frontend
|
|
*
|
|
* Task management view with:
|
|
* - All tasks extracted from emails
|
|
* - Deadline tracking and reminders
|
|
* - Priority-based organization
|
|
*
|
|
* See: docs/klausur-modul/UNIFIED-INBOX-SPECIFICATION.md
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import {
|
|
API_BASE,
|
|
Task,
|
|
DashboardStats,
|
|
FilterStatus,
|
|
FilterPriority,
|
|
statusLabels,
|
|
priorityLabels,
|
|
} from './_components/types'
|
|
import { StatCard } from './_components/StatCard'
|
|
import { CreateTaskModal } from './_components/CreateTaskModal'
|
|
import { TaskCard } from './_components/TaskCard'
|
|
|
|
export default function TasksPage() {
|
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
const [stats, setStats] = useState<DashboardStats | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [filterStatus, setFilterStatus] = useState<FilterStatus>('all')
|
|
const [filterPriority, setFilterPriority] = useState<FilterPriority>('all')
|
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
|
|
const fetchTasks = useCallback(async () => {
|
|
try {
|
|
setLoading(true)
|
|
const params = new URLSearchParams()
|
|
if (filterStatus !== 'all') params.append('status', filterStatus)
|
|
if (filterPriority !== 'all') params.append('priority', filterPriority)
|
|
params.append('include_completed', filterStatus === 'completed' ? 'true' : 'false')
|
|
|
|
const [tasksRes, statsRes] = await Promise.all([
|
|
fetch(`${API_BASE}/api/v1/mail/tasks?${params}`),
|
|
fetch(`${API_BASE}/api/v1/mail/tasks/dashboard`),
|
|
])
|
|
|
|
if (tasksRes.ok) { const data = await tasksRes.json(); setTasks(data.tasks || []) }
|
|
if (statsRes.ok) { const data = await statsRes.json(); setStats(data) }
|
|
} catch (err) {
|
|
console.error('Failed to fetch tasks:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [filterStatus, filterPriority])
|
|
|
|
useEffect(() => { fetchTasks() }, [fetchTasks])
|
|
|
|
const updateTaskStatus = async (taskId: string, status: string) => {
|
|
try {
|
|
await fetch(`${API_BASE}/api/v1/mail/tasks/${taskId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status }),
|
|
})
|
|
fetchTasks()
|
|
} catch (err) {
|
|
console.error('Failed to update task:', err)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-100">
|
|
{/* Header */}
|
|
<header className="bg-white border-b border-slate-200 px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-semibold text-slate-900">Arbeitsvorrat</h1>
|
|
<p className="text-sm text-slate-500">Aufgaben aus E-Mails und manuelle Eintraege</p>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<a href="/mail" className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50">Zurueck zur Inbox</a>
|
|
<button onClick={() => setShowCreateModal(true)} className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-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>
|
|
Aufgabe erstellen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="p-6">
|
|
{/* Dashboard Stats */}
|
|
{stats && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 mb-6">
|
|
<StatCard label="Gesamt" value={stats.totalTasks} />
|
|
<StatCard label="Offen" value={stats.pendingTasks} color="blue" />
|
|
<StatCard label="In Bearbeitung" value={stats.inProgressTasks} color="yellow" />
|
|
<StatCard label="Erledigt" value={stats.completedTasks} color="green" />
|
|
<StatCard label="Ueberfaellig" value={stats.overdueTasks} color="red" highlight={stats.overdueTasks > 0} />
|
|
<StatCard label="Heute faellig" value={stats.dueToday} color="orange" highlight={stats.dueToday > 0} />
|
|
<StatCard label="Diese Woche" value={stats.dueThisWeek} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white rounded-lg shadow p-4 mb-6">
|
|
<div className="flex flex-wrap gap-4 items-center">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-slate-700">Status:</span>
|
|
<div className="flex gap-1">
|
|
{(['all', 'pending', 'in_progress', 'completed'] as FilterStatus[]).map((status) => (
|
|
<button key={status} onClick={() => setFilterStatus(status)}
|
|
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${filterStatus === status ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}>
|
|
{status === 'all' ? 'Alle' : statusLabels[status]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-slate-700">Prioritaet:</span>
|
|
<div className="flex gap-1">
|
|
{(['all', 'urgent', 'high', 'medium', 'low'] as FilterPriority[]).map((priority) => (
|
|
<button key={priority} onClick={() => setFilterPriority(priority)}
|
|
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${filterPriority === priority ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}>
|
|
{priority === 'all' ? 'Alle' : priorityLabels[priority]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Task List */}
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
) : tasks.length === 0 ? (
|
|
<div className="bg-white rounded-lg shadow p-12 text-center">
|
|
<svg className="w-16 h-16 text-slate-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
<h3 className="text-lg font-medium text-slate-900 mb-2">Keine Aufgaben</h3>
|
|
<p className="text-slate-500">
|
|
{filterStatus !== 'all' || filterPriority !== 'all'
|
|
? 'Keine Aufgaben mit den gewaehlten Filtern gefunden.'
|
|
: 'Lassen Sie E-Mails analysieren oder erstellen Sie Aufgaben manuell.'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{tasks.map((task) => (
|
|
<TaskCard key={task.id} task={task} onUpdateStatus={updateTaskStatus} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showCreateModal && (
|
|
<CreateTaskModal
|
|
onClose={() => setShowCreateModal(false)}
|
|
onSuccess={() => { setShowCreateModal(false); fetchTasks() }}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|