Files
breakpilot-compliance/admin-compliance/app/sdk/process-tasks/page.tsx
Sharang Parnerkar 1fcd8244b1 refactor(admin): split evidence, process-tasks, iace/hazards pages
Extract components and hooks into _components/ and _hooks/ subdirectories
to reduce each page.tsx to under 500 LOC (was 1545/1383/1316).

Final line counts: evidence=213, process-tasks=304, hazards=157.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:12:15 +02:00

305 lines
16 KiB
TypeScript

'use client'
import React from 'react'
import { Toast } from './_components/Toast'
import { CompleteModal } from './_components/CompleteModal'
import { SkipModal } from './_components/SkipModal'
import { TaskFormModal } from './_components/TaskFormModal'
import { TaskDetailModal } from './_components/TaskDetailModal'
import { CalendarView } from './_components/CalendarView'
import {
CATEGORY_LABELS, CATEGORY_COLORS, STATUS_LABELS, STATUS_COLORS,
STATUS_ICONS, FREQUENCY_LABELS, formatDate, dueLabel, dueLabelColor,
} from './_components/types'
import { useProcessTasks } from './_hooks/useProcessTasks'
export default function ProcessTasksPage() {
const p = useProcessTasks()
return (
<div className="max-w-7xl mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Compliance Process Manager</h1>
<p className="text-sm text-gray-500 mt-1">Wiederkehrende Compliance-Aufgaben verwalten und nachverfolgen</p>
</div>
<div className="flex gap-2">
<button onClick={p.handleSeed}
className="px-4 py-2 text-sm text-purple-600 border border-purple-200 rounded-lg hover:bg-purple-50">
Standard-Aufgaben laden
</button>
<button onClick={() => p.setShowForm(true)}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700">
+ Neue Aufgabe
</button>
</div>
</div>
<div className="flex gap-1 bg-gray-100 rounded-xl p-1 mb-6 w-fit">
{[{ key: 'overview' as const, label: 'Uebersicht' }, { key: 'all' as const, label: 'Alle Aufgaben' }, { key: 'calendar' as const, label: 'Kalender' }].map(tab => (
<button key={tab.key} onClick={() => p.setActiveTab(tab.key)}
className={`px-4 py-2 text-sm rounded-lg transition-colors ${p.activeTab === tab.key ? 'bg-white text-gray-900 shadow-sm font-medium' : 'text-gray-500 hover:text-gray-700'}`}>
{tab.label}
</button>
))}
</div>
{p.activeTab === 'overview' && (
<div className="space-y-6">
{p.stats ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-red-200 p-5">
<div className="text-sm text-red-600 font-medium">Ueberfaellig</div>
<div className="text-3xl font-bold text-red-700 mt-1">{p.stats.overdue_count}</div>
<div className="text-xs text-gray-500 mt-1">Sofortiger Handlungsbedarf</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-5">
<div className="text-sm text-orange-600 font-medium">Diese Woche</div>
<div className="text-3xl font-bold text-orange-700 mt-1">{p.stats.due_7_days}</div>
<div className="text-xs text-gray-500 mt-1">Naechste 7 Tage</div>
</div>
<div className="bg-white rounded-xl border border-yellow-200 p-5">
<div className="text-sm text-yellow-600 font-medium">Diesen Monat</div>
<div className="text-3xl font-bold text-yellow-700 mt-1">{p.stats.due_30_days}</div>
<div className="text-xs text-gray-500 mt-1">Naechste 30 Tage</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-5">
<div className="text-sm text-green-600 font-medium">Erledigt</div>
<div className="text-3xl font-bold text-green-700 mt-1">{p.stats.by_status.completed || 0}</div>
<div className="text-xs text-gray-500 mt-1">von {p.stats.total} Aufgaben</div>
</div>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map(i => (
<div key={i} className="bg-white rounded-xl border p-5 animate-pulse">
<div className="h-4 bg-gray-200 rounded w-24 mb-2"></div>
<div className="h-8 bg-gray-200 rounded w-12 mb-1"></div>
<div className="h-3 bg-gray-200 rounded w-32"></div>
</div>
))}
</div>
)}
{p.stats && Object.keys(p.stats.by_category).length > 0 && (
<div className="bg-white rounded-xl border p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Nach Kategorie</h3>
<div className="flex flex-wrap gap-2">
{Object.entries(p.stats.by_category).map(([cat, count]) => (
<span key={cat} className={`px-3 py-1.5 text-sm rounded-lg ${CATEGORY_COLORS[cat] || 'bg-gray-100 text-gray-600'}`}>
{CATEGORY_LABELS[cat] || cat}: {count}
</span>
))}
</div>
</div>
)}
<div className="bg-white rounded-xl border">
<div className="p-5 border-b">
<h3 className="text-sm font-semibold text-gray-900">Naechste faellige Aufgaben</h3>
</div>
{p.upcomingTasks.length === 0 ? (
<div className="p-8 text-center text-gray-400">
<p className="text-sm">Keine Aufgaben in den naechsten 30 Tagen faellig.</p>
{p.stats && p.stats.total === 0 && (
<button onClick={p.handleSeed}
className="mt-3 px-4 py-2 text-sm text-purple-600 border border-purple-200 rounded-lg hover:bg-purple-50">
Standard-Aufgaben laden
</button>
)}
</div>
) : (
<div className="divide-y">
{p.upcomingTasks.slice(0, 5).map(t => (
<div key={t.id} className="flex items-center gap-4 p-4 hover:bg-gray-50 cursor-pointer" onClick={() => p.setDetailTask(t)}>
<span className={`w-8 h-8 flex items-center justify-center rounded-full text-sm ${STATUS_COLORS[t.status]}`}>
{STATUS_ICONS[t.status]}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{t.title}</p>
<div className="flex items-center gap-2 mt-0.5">
<span className={`text-xs rounded px-1.5 py-0.5 ${CATEGORY_COLORS[t.category]}`}>{CATEGORY_LABELS[t.category]}</span>
<span className="text-xs text-gray-400">{FREQUENCY_LABELS[t.frequency]}</span>
</div>
</div>
<div className="text-right flex-shrink-0">
<p className={`text-sm ${dueLabelColor(t.next_due_date)}`}>{dueLabel(t.next_due_date)}</p>
<p className="text-xs text-gray-400">{formatDate(t.next_due_date)}</p>
</div>
<button onClick={e => { e.stopPropagation(); p.setCompleteTask(t) }}
className="px-3 py-1 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 flex-shrink-0">
Erledigen
</button>
</div>
))}
</div>
)}
</div>
</div>
)}
{p.activeTab === 'all' && (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-3">
<select value={p.filterStatus} onChange={e => { p.setFilterStatus(e.target.value); p.setPage(0) }}
className="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white">
<option value="">Alle Status</option>
{Object.entries(STATUS_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
<select value={p.filterCategory} onChange={e => { p.setFilterCategory(e.target.value); p.setPage(0) }}
className="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white">
<option value="">Alle Kategorien</option>
{Object.entries(CATEGORY_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
<select value={p.filterFrequency} onChange={e => { p.setFilterFrequency(e.target.value); p.setPage(0) }}
className="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white">
<option value="">Alle Frequenzen</option>
{Object.entries(FREQUENCY_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
<span className="text-sm text-gray-400 ml-auto">{p.totalTasks} Aufgaben</span>
</div>
<div className="bg-white rounded-xl border overflow-hidden">
{p.loading ? (
<div className="p-6 space-y-3">
{[1, 2, 3, 4, 5].map(i => (
<div key={i} className="flex gap-4 animate-pulse">
<div className="h-8 w-8 bg-gray-200 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-3 bg-gray-200 rounded w-1/4"></div>
</div>
</div>
))}
</div>
) : p.tasks.length === 0 ? (
<div className="p-12 text-center text-gray-400">
<p className="text-lg mb-1">Keine Aufgaben gefunden</p>
<p className="text-sm">Erstellen Sie eine neue Aufgabe oder laden Sie die Standard-Aufgaben.</p>
</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-gray-50">
<th className="text-left py-3 px-4 font-medium text-gray-500 w-10">Status</th>
<th className="text-left py-3 px-4 font-medium text-gray-500">Titel</th>
<th className="text-left py-3 px-4 font-medium text-gray-500">Kategorie</th>
<th className="text-left py-3 px-4 font-medium text-gray-500">Frequenz</th>
<th className="text-left py-3 px-4 font-medium text-gray-500">Faellig am</th>
<th className="text-left py-3 px-4 font-medium text-gray-500">Zustaendig</th>
<th className="text-right py-3 px-4 font-medium text-gray-500">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y">
{p.tasks.map(t => (
<tr key={t.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => p.setDetailTask(t)}>
<td className="py-3 px-4">
<span className={`w-7 h-7 flex items-center justify-center rounded-full text-xs ${STATUS_COLORS[t.status]}`}>
{STATUS_ICONS[t.status]}
</span>
</td>
<td className="py-3 px-4">
<p className="font-medium text-gray-900">{t.title}</p>
<p className="text-xs text-gray-400">{t.task_code}</p>
</td>
<td className="py-3 px-4">
<span className={`px-2 py-0.5 text-xs rounded-full ${CATEGORY_COLORS[t.category]}`}>
{CATEGORY_LABELS[t.category] || t.category}
</span>
</td>
<td className="py-3 px-4 text-gray-600">{FREQUENCY_LABELS[t.frequency] || t.frequency}</td>
<td className="py-3 px-4">
<span className={dueLabelColor(t.next_due_date)}>{formatDate(t.next_due_date)}</span>
<p className={`text-xs ${dueLabelColor(t.next_due_date)}`}>{dueLabel(t.next_due_date)}</p>
</td>
<td className="py-3 px-4 text-gray-600">{t.assigned_to || '\u2014'}</td>
<td className="py-3 px-4 text-right" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1">
{t.status !== 'completed' && (
<button onClick={() => p.setCompleteTask(t)}
className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700" title="Erledigen">
{'\u2714'}
</button>
)}
{t.status !== 'completed' && (
<button onClick={() => p.setSkipTask(t)}
className="px-2 py-1 text-xs bg-yellow-500 text-white rounded hover:bg-yellow-600" title="Ueberspringen">
{'\u2192'}
</button>
)}
<button onClick={() => p.setEditTask(t)}
className="px-2 py-1 text-xs text-gray-500 hover:bg-gray-100 rounded border" title="Bearbeiten">
{'\u270E'}
</button>
<button onClick={() => { if (confirm('Aufgabe wirklich loeschen?')) p.handleDelete(t.id) }}
className="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded border border-red-200" title="Loeschen">
{'\u2716'}
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{p.totalPages > 1 && (
<div className="flex items-center justify-between">
<button onClick={() => p.setPage(prev => Math.max(0, prev - 1))} disabled={p.page === 0}
className="px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded-lg border disabled:opacity-40">
Zurueck
</button>
<span className="text-sm text-gray-500">Seite {p.page + 1} von {p.totalPages}</span>
<button onClick={() => p.setPage(prev => Math.min(p.totalPages - 1, prev + 1))} disabled={p.page >= p.totalPages - 1}
className="px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded-lg border disabled:opacity-40">
Weiter
</button>
</div>
)}
</div>
)}
{p.activeTab === 'calendar' && (
<div className="bg-white rounded-xl border p-5">
<CalendarView tasks={[...p.tasks, ...p.upcomingTasks].filter((t, i, arr) => arr.findIndex(x => x.id === t.id) === i)} />
</div>
)}
{p.showForm && <TaskFormModal onClose={() => p.setShowForm(false)} onSave={p.handleCreate} />}
{p.editTask && (
<TaskFormModal
initial={{
task_code: p.editTask.task_code, title: p.editTask.title,
description: p.editTask.description || '', category: p.editTask.category,
priority: p.editTask.priority, frequency: p.editTask.frequency,
assigned_to: p.editTask.assigned_to || '', responsible_team: p.editTask.responsible_team || '',
linked_module: p.editTask.linked_module || '',
next_due_date: p.editTask.next_due_date ? p.editTask.next_due_date.substring(0, 10) : '',
due_reminder_days: p.editTask.due_reminder_days, notes: p.editTask.notes || '',
}}
onClose={() => p.setEditTask(null)} onSave={p.handleUpdate} />
)}
{p.detailTask && (
<TaskDetailModal task={p.detailTask} onClose={() => p.setDetailTask(null)}
onComplete={t => { p.setDetailTask(null); p.setCompleteTask(t) }}
onSkip={t => { p.setDetailTask(null); p.setSkipTask(t) }}
onEdit={t => { p.setDetailTask(null); p.setEditTask(t) }}
onDelete={p.handleDelete} />
)}
{p.completeTask && (
<CompleteModal task={p.completeTask} onClose={() => p.setCompleteTask(null)} onComplete={p.handleComplete} />
)}
{p.skipTask && (
<SkipModal task={p.skipTask} onClose={() => p.setSkipTask(null)} onSkip={p.handleSkip} />
)}
{p.toast && <Toast message={p.toast} onClose={() => p.setToast(null)} />}
</div>
)
}