Extract ObligationModal, ObligationDetail, ObligationCard, ObligationsHeader, StatsGrid, FilterBar and InfoBanners into _components/, plus _types.ts for shared types/constants. page.tsx drops from 987 to 325 LOC, below the 300 soft target region and well under the 500 hard cap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
137 lines
5.5 KiB
TypeScript
137 lines
5.5 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import {
|
|
PRIORITY_COLORS,
|
|
PRIORITY_LABELS,
|
|
STATUS_COLORS,
|
|
STATUS_LABELS,
|
|
STATUS_NEXT,
|
|
type Obligation,
|
|
} from '../_types'
|
|
|
|
export default function ObligationDetail({ obligation, onClose, onStatusChange, onEdit, onDelete }: {
|
|
obligation: Obligation
|
|
onClose: () => void
|
|
onStatusChange: (id: string, status: string) => Promise<void>
|
|
onEdit: () => void
|
|
onDelete: (id: string) => Promise<void>
|
|
}) {
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
const handleStatusCycle = async () => {
|
|
setSaving(true)
|
|
await onStatusChange(obligation.id, STATUS_NEXT[obligation.status])
|
|
setSaving(false)
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
if (!confirm('Pflicht wirklich loeschen?')) return
|
|
await onDelete(obligation.id)
|
|
onClose()
|
|
}
|
|
|
|
const daysUntil = obligation.deadline
|
|
? Math.ceil((new Date(obligation.deadline).getTime() - Date.now()) / 86400000)
|
|
: null
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/40 z-50 flex items-end md: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-1 flex-wrap">
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${PRIORITY_COLORS[obligation.priority]}`}>{PRIORITY_LABELS[obligation.priority]}</span>
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${STATUS_COLORS[obligation.status]}`}>{STATUS_LABELS[obligation.status]}</span>
|
|
{obligation.source && (
|
|
<span className="px-2 py-0.5 text-xs bg-purple-50 text-purple-700 rounded">{obligation.source} {obligation.source_article}</span>
|
|
)}
|
|
</div>
|
|
<h2 className="text-base font-semibold text-gray-900">{obligation.title}</h2>
|
|
</div>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 flex-shrink-0">✕</button>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-4 text-sm">
|
|
{obligation.description && (
|
|
<p className="text-gray-700 whitespace-pre-wrap">{obligation.description}</p>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<span className="text-gray-500">Verantwortlich</span>
|
|
<p className="font-medium text-gray-900">{obligation.responsible || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Frist</span>
|
|
<p className={`font-medium ${daysUntil !== null && daysUntil < 0 ? 'text-red-600' : 'text-gray-900'}`}>
|
|
{obligation.deadline
|
|
? `${new Date(obligation.deadline).toLocaleDateString('de-DE')}${daysUntil !== null ? ` (${daysUntil < 0 ? `${Math.abs(daysUntil)}d ueberfaellig` : `${daysUntil}d`})` : ''}`
|
|
: '—'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{obligation.linked_systems?.length > 0 && (
|
|
<div>
|
|
<span className="text-gray-500">Betroffene Systeme</span>
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{obligation.linked_systems.map(s => (
|
|
<span key={s} className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{s}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{obligation.notes && (
|
|
<div>
|
|
<span className="text-gray-500">Notizen</span>
|
|
<p className="text-gray-700 mt-1">{obligation.notes}</p>
|
|
</div>
|
|
)}
|
|
|
|
{obligation.rule_code && (
|
|
<div className="bg-purple-50 rounded-lg p-3">
|
|
<span className="text-xs text-purple-600">Aus UCCA Assessment abgeleitet · Regel {obligation.rule_code}</span>
|
|
</div>
|
|
)}
|
|
|
|
{obligation.created_at && (
|
|
<p className="text-xs text-gray-400">
|
|
Erstellt: {new Date(obligation.created_at).toLocaleDateString('de-DE')}
|
|
{obligation.updated_at && obligation.updated_at !== obligation.created_at
|
|
? ` · Geaendert: ${new Date(obligation.updated_at).toLocaleDateString('de-DE')}`
|
|
: ''}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-2 p-6 border-t flex-wrap">
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={handleDelete}
|
|
className="px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 rounded-lg border border-red-200"
|
|
>
|
|
Loeschen
|
|
</button>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={onEdit} className="px-3 py-1.5 text-xs text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg">
|
|
Bearbeiten
|
|
</button>
|
|
{obligation.status !== 'completed' && (
|
|
<button
|
|
onClick={handleStatusCycle}
|
|
disabled={saving}
|
|
className="px-4 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
{saving ? '...' : STATUS_NEXT[obligation.status] === 'completed' ? 'Als erledigt markieren' : `→ ${STATUS_LABELS[STATUS_NEXT[obligation.status]]}`}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|