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>
414 lines
16 KiB
TypeScript
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>
|
|
)
|
|
}
|