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