Files
breakpilot-lehrer/website/app/admin/compliance/my-tasks/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

414 lines
16 KiB
TypeScript

'use client'
/**
* Meine Aufgaben - Personal Task Dashboard
*
* Zeigt dem angemeldeten Benutzer seine Compliance-Aufgaben:
* - Offene Control-Reviews
* - Faellige Evidence-Uploads
* - Ausstehende Sign-offs
* - Risiko-Behandlungen
*/
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import AdminLayout from '@/components/admin/AdminLayout'
import { Language } from '@/lib/compliance-i18n'
interface Task {
id: string
type: 'control_review' | 'evidence_upload' | 'signoff' | 'risk_treatment'
title: string
description: string
priority: 'critical' | 'high' | 'medium' | 'low'
due_date: string | null
days_remaining: number | null
status: 'overdue' | 'due_soon' | 'pending' | 'in_progress'
related_entity: {
type: string
id: string
name: string
}
}
interface TaskStats {
total: number
overdue: number
due_soon: number
in_progress: number
completed_this_week: number
}
// Mock-Daten fuer die Demonstration
const MOCK_TASKS: Task[] = [
{
id: '1',
type: 'control_review',
title: 'PRIV-001 Review faellig',
description: 'Quartalsreview des Verarbeitungsverzeichnisses',
priority: 'high',
due_date: '2026-01-20',
days_remaining: 2,
status: 'due_soon',
related_entity: { type: 'control', id: 'PRIV-001', name: 'Verarbeitungsverzeichnis' }
},
{
id: '2',
type: 'evidence_upload',
title: 'SAST-Report hochladen',
description: 'Aktueller Semgrep-Scan fuer SDLC-001',
priority: 'medium',
due_date: '2026-01-25',
days_remaining: 7,
status: 'pending',
related_entity: { type: 'control', id: 'SDLC-001', name: 'SAST Scanning' }
},
{
id: '3',
type: 'signoff',
title: 'Audit Sign-off: DSGVO Art. 32',
description: 'Sign-off fuer technische Massnahmen im Q1 Audit',
priority: 'critical',
due_date: '2026-01-19',
days_remaining: 1,
status: 'due_soon',
related_entity: { type: 'requirement', id: 'gdpr-art32', name: 'DSGVO Art. 32' }
},
{
id: '4',
type: 'risk_treatment',
title: 'RISK-003 Behandlung',
description: 'Risiko-Behandlungsplan fuer Key-Rotation definieren',
priority: 'high',
due_date: '2026-01-22',
days_remaining: 4,
status: 'in_progress',
related_entity: { type: 'risk', id: 'RISK-003', name: 'Unzureichende Key-Rotation' }
},
{
id: '5',
type: 'control_review',
title: 'IAM-002 MFA-Check',
description: 'Ueberpruefung der MFA-Abdeckung fuer Admin-Accounts',
priority: 'medium',
due_date: '2026-01-28',
days_remaining: 10,
status: 'pending',
related_entity: { type: 'control', id: 'IAM-002', name: 'MFA fuer Admin-Accounts' }
},
{
id: '6',
type: 'evidence_upload',
title: 'Backup-Test Protokoll',
description: 'Nachweis fuer erfolgreichen Backup-Restore-Test',
priority: 'low',
due_date: '2026-02-01',
days_remaining: 14,
status: 'pending',
related_entity: { type: 'control', id: 'OPS-002', name: 'Backup & Recovery' }
},
]
const MOCK_STATS: TaskStats = {
total: 6,
overdue: 0,
due_soon: 2,
in_progress: 1,
completed_this_week: 3
}
const TYPE_LABELS = {
de: {
control_review: 'Control-Review',
evidence_upload: 'Nachweis-Upload',
signoff: 'Sign-off',
risk_treatment: 'Risiko-Behandlung'
},
en: {
control_review: 'Control Review',
evidence_upload: 'Evidence Upload',
signoff: 'Sign-off',
risk_treatment: 'Risk Treatment'
}
}
const PRIORITY_COLORS = {
critical: 'bg-red-500',
high: 'bg-orange-500',
medium: 'bg-yellow-500',
low: 'bg-slate-400'
}
const STATUS_COLORS = {
overdue: 'text-red-500 bg-red-500/10',
due_soon: 'text-orange-500 bg-orange-500/10',
pending: 'text-slate-400 bg-slate-400/10',
in_progress: 'text-blue-500 bg-blue-500/10'
}
export default function MyTasksPage() {
const router = useRouter()
const [language, setLanguage] = useState<Language>('de')
const [tasks, setTasks] = useState<Task[]>(MOCK_TASKS)
const [stats, setStats] = useState<TaskStats>(MOCK_STATS)
const [filter, setFilter] = useState<string>('all')
const [sortBy, setSortBy] = useState<'due_date' | 'priority'>('due_date')
const [loading, setLoading] = useState(false)
useEffect(() => {
const storedLang = localStorage.getItem('compliance_language') as Language
if (storedLang) {
setLanguage(storedLang)
}
// In Zukunft: Lade Tasks vom Backend
// loadTasks()
}, [])
const filteredTasks = tasks
.filter(task => filter === 'all' || task.type === filter)
.sort((a, b) => {
if (sortBy === 'due_date') {
const aDate = a.due_date ? new Date(a.due_date).getTime() : Infinity
const bDate = b.due_date ? new Date(b.due_date).getTime() : Infinity
return aDate - bDate
} else {
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }
return priorityOrder[a.priority] - priorityOrder[b.priority]
}
})
const getTypeIcon = (type: Task['type']) => {
switch (type) {
case 'control_review':
return (
<svg className="w-5 h-5" 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>
)
case 'evidence_upload':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
)
case 'signoff':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
)
case 'risk_treatment':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
)
}
}
return (
<AdminLayout>
<div className="p-6 bg-slate-900 min-h-screen">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-white">
{language === 'de' ? 'Meine Aufgaben' : 'My Tasks'}
</h1>
<p className="text-slate-400 mt-1">
{language === 'de'
? 'Uebersicht Ihrer Compliance-Aufgaben'
: 'Overview of your compliance tasks'}
</p>
</div>
<button
onClick={() => router.push('/admin/compliance/role-select')}
className="px-4 py-2 bg-slate-700 text-slate-300 rounded-lg hover:bg-slate-600 transition-colors"
>
{language === 'de' ? 'Zurueck zur Auswahl' : 'Back to Selection'}
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8">
<StatCard
label={language === 'de' ? 'Gesamt' : 'Total'}
value={stats.total}
color="text-white"
bgColor="bg-slate-700"
/>
<StatCard
label={language === 'de' ? 'Ueberfaellig' : 'Overdue'}
value={stats.overdue}
color="text-red-500"
bgColor="bg-red-500/10"
/>
<StatCard
label={language === 'de' ? 'Bald faellig' : 'Due Soon'}
value={stats.due_soon}
color="text-orange-500"
bgColor="bg-orange-500/10"
/>
<StatCard
label={language === 'de' ? 'In Bearbeitung' : 'In Progress'}
value={stats.in_progress}
color="text-blue-500"
bgColor="bg-blue-500/10"
/>
<StatCard
label={language === 'de' ? 'Diese Woche erledigt' : 'Completed This Week'}
value={stats.completed_this_week}
color="text-green-500"
bgColor="bg-green-500/10"
/>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex items-center gap-2">
<span className="text-slate-400 text-sm">
{language === 'de' ? 'Filter:' : 'Filter:'}
</span>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="bg-slate-800 border border-slate-700 text-white rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">{language === 'de' ? 'Alle' : 'All'}</option>
<option value="control_review">{TYPE_LABELS[language].control_review}</option>
<option value="evidence_upload">{TYPE_LABELS[language].evidence_upload}</option>
<option value="signoff">{TYPE_LABELS[language].signoff}</option>
<option value="risk_treatment">{TYPE_LABELS[language].risk_treatment}</option>
</select>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400 text-sm">
{language === 'de' ? 'Sortieren:' : 'Sort by:'}
</span>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'due_date' | 'priority')}
className="bg-slate-800 border border-slate-700 text-white rounded-lg px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
>
<option value="due_date">{language === 'de' ? 'Faelligkeitsdatum' : 'Due Date'}</option>
<option value="priority">{language === 'de' ? 'Prioritaet' : 'Priority'}</option>
</select>
</div>
</div>
{/* Task List */}
<div className="space-y-4">
{filteredTasks.length === 0 ? (
<div className="bg-slate-800 rounded-xl p-12 text-center">
<svg className="w-16 h-16 mx-auto text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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-xl font-semibold text-white mb-2">
{language === 'de' ? 'Keine Aufgaben' : 'No Tasks'}
</h3>
<p className="text-slate-400">
{language === 'de'
? 'Sie haben aktuell keine offenen Compliance-Aufgaben.'
: 'You currently have no open compliance tasks.'}
</p>
</div>
) : (
filteredTasks.map((task) => (
<div
key={task.id}
className="bg-slate-800 rounded-xl p-5 border border-slate-700 hover:border-slate-600 transition-colors"
>
<div className="flex items-start gap-4">
{/* Icon */}
<div className={`p-3 rounded-lg ${STATUS_COLORS[task.status]}`}>
{getTypeIcon(task.type)}
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<h3 className="text-lg font-semibold text-white">{task.title}</h3>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${PRIORITY_COLORS[task.priority]} text-white`}>
{task.priority.toUpperCase()}
</span>
<span className="text-xs text-slate-500 bg-slate-700 px-2 py-0.5 rounded">
{TYPE_LABELS[language][task.type]}
</span>
</div>
<p className="text-slate-400 text-sm mb-2">{task.description}</p>
<div className="flex items-center gap-4 text-sm">
<span className="text-slate-500">
{language === 'de' ? 'Betrifft:' : 'Related:'}{' '}
<span className="text-blue-400">{task.related_entity.name}</span>
</span>
{task.due_date && (
<span className={task.days_remaining !== null && task.days_remaining <= 3 ? 'text-orange-400' : 'text-slate-500'}>
{language === 'de' ? 'Faellig:' : 'Due:'}{' '}
{new Date(task.due_date).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
{task.days_remaining !== null && (
<span className="ml-1">
({task.days_remaining} {language === 'de' ? 'Tage' : 'days'})
</span>
)}
</span>
)}
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<button
onClick={() => {
// Navigation basierend auf Task-Typ
if (task.type === 'signoff') {
router.push('/admin/compliance/audit-checklist')
} else if (task.type === 'evidence_upload') {
router.push('/admin/compliance/evidence')
} else if (task.type === 'control_review') {
router.push('/admin/compliance/controls')
} else {
router.push('/admin/compliance/risks')
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-500 transition-colors"
>
{language === 'de' ? 'Bearbeiten' : 'Handle'}
</button>
<button
className="px-3 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition-colors"
title={language === 'de' ? 'Spaeter erledigen' : 'Snooze'}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
</div>
</div>
))
)}
</div>
{/* Footer */}
<div className="mt-8 text-center text-slate-500 text-sm">
<p>
{language === 'de'
? 'Aufgaben werden automatisch basierend auf Control-Review-Zyklen, Evidence-Ablauf und Audit-Sessions generiert.'
: 'Tasks are automatically generated based on control review cycles, evidence expiry, and audit sessions.'}
</p>
</div>
</div>
</AdminLayout>
)
}
// Stat Card Component
function StatCard({ label, value, color, bgColor }: { label: string; value: number; color: string; bgColor: string }) {
return (
<div className={`${bgColor} rounded-xl p-4 border border-slate-700`}>
<p className="text-slate-400 text-sm mb-1">{label}</p>
<p className={`text-3xl font-bold ${color}`}>{value}</p>
</div>
)
}