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>
166 lines
6.8 KiB
TypeScript
166 lines
6.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import {
|
|
ProcessTask, HistoryEntry, API,
|
|
CATEGORY_LABELS, CATEGORY_COLORS, PRIORITY_LABELS, PRIORITY_COLORS,
|
|
STATUS_LABELS, STATUS_COLORS, FREQUENCY_LABELS,
|
|
formatDate, dueLabel, dueLabelColor,
|
|
} from './types'
|
|
|
|
export function TaskDetailModal({
|
|
task,
|
|
onClose,
|
|
onComplete,
|
|
onSkip,
|
|
onEdit,
|
|
onDelete,
|
|
}: {
|
|
task: ProcessTask
|
|
onClose: () => void
|
|
onComplete: (task: ProcessTask) => void
|
|
onSkip: (task: ProcessTask) => void
|
|
onEdit: (task: ProcessTask) => void
|
|
onDelete: (id: string) => Promise<void>
|
|
}) {
|
|
const [history, setHistory] = useState<HistoryEntry[]>([])
|
|
const [loadingHistory, setLoadingHistory] = useState(true)
|
|
|
|
useEffect(() => {
|
|
loadHistory()
|
|
}, [task.id]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const loadHistory = async () => {
|
|
try {
|
|
const res = await fetch(`${API}/${task.id}/history`)
|
|
if (res.ok) { const data = await res.json(); setHistory(data.history || []) }
|
|
} catch { /* ignore */ }
|
|
setLoadingHistory(false)
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
if (!confirm('Aufgabe wirklich loeschen?')) return
|
|
await onDelete(task.id)
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={e => e.target === e.currentTarget && onClose()}>
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[85vh] overflow-y-auto">
|
|
<div className="flex items-start justify-between p-6 border-b gap-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${CATEGORY_COLORS[task.category] || 'bg-gray-100 text-gray-600'}`}>
|
|
{CATEGORY_LABELS[task.category] || task.category}
|
|
</span>
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${PRIORITY_COLORS[task.priority]}`}>
|
|
{PRIORITY_LABELS[task.priority]}
|
|
</span>
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${STATUS_COLORS[task.status]}`}>
|
|
{STATUS_LABELS[task.status]}
|
|
</span>
|
|
</div>
|
|
<h2 className="text-base font-semibold text-gray-900">{task.title}</h2>
|
|
<p className="text-xs text-gray-400 mt-0.5">{task.task_code}</p>
|
|
</div>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 flex-shrink-0">{'\u2715'}</button>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-4 text-sm">
|
|
{task.description && <p className="text-gray-700 whitespace-pre-wrap">{task.description}</p>}
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<span className="text-gray-500">Frequenz</span>
|
|
<p className="font-medium text-gray-900">{FREQUENCY_LABELS[task.frequency] || task.frequency}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Faellig</span>
|
|
<p className={`font-medium ${dueLabelColor(task.next_due_date)}`}>
|
|
{task.next_due_date ? `${formatDate(task.next_due_date)} (${dueLabel(task.next_due_date)})` : '\u2014'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Zustaendig</span>
|
|
<p className="font-medium text-gray-900">{task.assigned_to || '\u2014'}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Team</span>
|
|
<p className="font-medium text-gray-900">{task.responsible_team || '\u2014'}</p>
|
|
</div>
|
|
{task.linked_module && (
|
|
<div>
|
|
<span className="text-gray-500">Modul</span>
|
|
<p className="font-medium text-purple-700">{task.linked_module}</p>
|
|
</div>
|
|
)}
|
|
{task.last_completed_at && (
|
|
<div>
|
|
<span className="text-gray-500">Zuletzt erledigt</span>
|
|
<p className="font-medium text-gray-900">{formatDate(task.last_completed_at)}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{task.notes && (
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<span className="text-gray-500 text-xs">Notizen</span>
|
|
<p className="text-gray-700 mt-1">{task.notes}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="border-t pt-4">
|
|
<h3 className="font-semibold text-gray-900 mb-2">Verlauf</h3>
|
|
{loadingHistory ? (
|
|
<div className="animate-pulse space-y-2">
|
|
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
|
</div>
|
|
) : history.length === 0 ? (
|
|
<p className="text-gray-400 text-sm">Noch keine Eintraege</p>
|
|
) : (
|
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
|
{history.map(h => (
|
|
<div key={h.id} className="flex items-start gap-2 text-xs bg-gray-50 rounded-lg p-2">
|
|
<span className={`px-1.5 py-0.5 rounded ${h.status === 'completed' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}`}>
|
|
{h.status === 'completed' ? 'Erledigt' : 'Uebersprungen'}
|
|
</span>
|
|
<div className="flex-1">
|
|
<p className="text-gray-600">{formatDate(h.completed_at)} {h.completed_by ? `von ${h.completed_by}` : ''}</p>
|
|
{h.result && <p className="text-gray-700 mt-0.5">{h.result}</p>}
|
|
{h.notes && <p className="text-gray-500 mt-0.5">{h.notes}</p>}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 p-6 border-t flex-wrap">
|
|
{task.status !== 'completed' && (
|
|
<>
|
|
<button onClick={() => { onClose(); onComplete(task) }}
|
|
className="px-3 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700">
|
|
Erledigen
|
|
</button>
|
|
<button onClick={() => { onClose(); onSkip(task) }}
|
|
className="px-3 py-1.5 text-sm bg-yellow-500 text-white rounded-lg hover:bg-yellow-600">
|
|
Ueberspringen
|
|
</button>
|
|
</>
|
|
)}
|
|
<button onClick={() => { onClose(); onEdit(task) }}
|
|
className="px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded-lg border">
|
|
Bearbeiten
|
|
</button>
|
|
<button onClick={handleDelete}
|
|
className="px-3 py-1.5 text-sm text-red-600 hover:bg-red-50 rounded-lg border border-red-200 ml-auto">
|
|
Loeschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|