refactor(admin): split loeschfristen + dsb-portal page.tsx into colocated components
Split two oversized page files into _components/ directories following Next.js 15 conventions and the 500-LOC hard cap: - loeschfristen/page.tsx (2322 LOC -> 412 LOC orchestrator + 6 components) - dsb-portal/page.tsx (2068 LOC -> 135 LOC orchestrator + 9 components) All component files stay under 500 lines. Build verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
189
admin-compliance/app/sdk/dsb-portal/_components/AufgabenTab.tsx
Normal file
189
admin-compliance/app/sdk/dsb-portal/_components/AufgabenTab.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Task, TASK_CATEGORIES, PRIORITY_LABELS, PRIORITY_COLORS,
|
||||
TASK_STATUS_LABELS, TASK_STATUS_COLORS, apiFetch, formatDate,
|
||||
} from './types'
|
||||
import {
|
||||
Skeleton, Modal, Badge, FormLabel, FormInput, FormTextarea,
|
||||
FormSelect, PrimaryButton, SecondaryButton, ErrorState, EmptyState,
|
||||
IconTask, IconPlus, IconCheck, IconCalendar,
|
||||
} from './ui-primitives'
|
||||
|
||||
export function AufgabenTab({
|
||||
assignmentId,
|
||||
addToast,
|
||||
}: {
|
||||
assignmentId: string
|
||||
addToast: (msg: string, type?: 'success' | 'error') => void
|
||||
}) {
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
const [newDesc, setNewDesc] = useState('')
|
||||
const [newCategory, setNewCategory] = useState(TASK_CATEGORIES[0])
|
||||
const [newPriority, setNewPriority] = useState('medium')
|
||||
const [newDueDate, setNewDueDate] = useState('')
|
||||
|
||||
const fetchTasks = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const params = statusFilter !== 'all' ? `?status=${statusFilter}` : ''
|
||||
const data = await apiFetch<Task[]>(`/api/sdk/v1/dsb/assignments/${assignmentId}/tasks${params}`)
|
||||
setTasks(data)
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Laden der Aufgaben')
|
||||
} finally { setLoading(false) }
|
||||
}, [assignmentId, statusFilter])
|
||||
|
||||
useEffect(() => { fetchTasks() }, [fetchTasks])
|
||||
|
||||
const handleCreateTask = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
try {
|
||||
await apiFetch<Task>(`/api/sdk/v1/dsb/assignments/${assignmentId}/tasks`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: newTitle, description: newDesc, category: newCategory,
|
||||
priority: newPriority, due_date: newDueDate || null,
|
||||
}),
|
||||
})
|
||||
addToast('Aufgabe erstellt')
|
||||
setShowModal(false)
|
||||
setNewTitle(''); setNewDesc(''); setNewCategory(TASK_CATEGORIES[0])
|
||||
setNewPriority('medium'); setNewDueDate('')
|
||||
fetchTasks()
|
||||
} catch (e: unknown) {
|
||||
addToast(e instanceof Error ? e.message : 'Fehler', 'error')
|
||||
} finally { setSaving(false) }
|
||||
}
|
||||
|
||||
const handleCompleteTask = async (taskId: string) => {
|
||||
try {
|
||||
await apiFetch(`/api/sdk/v1/dsb/tasks/${taskId}/complete`, { method: 'POST' })
|
||||
addToast('Aufgabe abgeschlossen')
|
||||
fetchTasks()
|
||||
} catch (e: unknown) {
|
||||
addToast(e instanceof Error ? e.message : 'Fehler', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const statusFilters = [
|
||||
{ value: 'all', label: 'Alle' },
|
||||
{ value: 'open', label: 'Offen' },
|
||||
{ value: 'in_progress', label: 'In Bearbeitung' },
|
||||
{ value: 'completed', label: 'Erledigt' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{statusFilters.map((f) => (
|
||||
<button key={f.value} onClick={() => setStatusFilter(f.value)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||||
statusFilter === f.value ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}>{f.label}</button>
|
||||
))}
|
||||
</div>
|
||||
<PrimaryButton onClick={() => setShowModal(true)} className="flex items-center gap-1.5">
|
||||
<IconPlus /> Neue Aufgabe
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-16 rounded-lg" />)}
|
||||
</div>
|
||||
) : error ? (
|
||||
<ErrorState message={error} onRetry={fetchTasks} />
|
||||
) : tasks.length === 0 ? (
|
||||
<EmptyState icon={<IconTask className="w-7 h-7" />} title="Keine Aufgaben"
|
||||
description="Erstellen Sie eine neue Aufgabe um zu beginnen." />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{tasks.map((task) => (
|
||||
<div key={task.id} className="bg-white border border-gray-200 rounded-lg p-4 hover:border-purple-300 transition-colors">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className={`font-medium ${task.status === 'completed' ? 'line-through text-gray-400' : 'text-gray-900'}`}>
|
||||
{task.title}
|
||||
</h4>
|
||||
<Badge label={task.category} className="bg-purple-50 text-purple-600 border-purple-200" />
|
||||
<Badge label={PRIORITY_LABELS[task.priority] || task.priority}
|
||||
className={PRIORITY_COLORS[task.priority] || 'bg-gray-100 text-gray-500'} />
|
||||
<Badge label={TASK_STATUS_LABELS[task.status] || task.status}
|
||||
className={TASK_STATUS_COLORS[task.status] || 'bg-gray-100 text-gray-500'} />
|
||||
</div>
|
||||
{task.description && <p className="text-sm text-gray-500 mt-1 line-clamp-2">{task.description}</p>}
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-gray-400">
|
||||
{task.due_date && (
|
||||
<span className="flex items-center gap-1"><IconCalendar className="w-3 h-3" />Frist: {formatDate(task.due_date)}</span>
|
||||
)}
|
||||
<span>Erstellt: {formatDate(task.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{task.status !== 'completed' && task.status !== 'cancelled' && (
|
||||
<button onClick={() => handleCompleteTask(task.id)} title="Aufgabe abschliessen"
|
||||
className="p-2 rounded-lg text-green-600 hover:bg-green-50 transition-colors flex-shrink-0">
|
||||
<IconCheck className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create task modal */}
|
||||
<Modal open={showModal} onClose={() => setShowModal(false)} title="Neue Aufgabe erstellen">
|
||||
<form onSubmit={handleCreateTask} className="space-y-4">
|
||||
<div>
|
||||
<FormLabel htmlFor="task-title">Titel *</FormLabel>
|
||||
<FormInput id="task-title" value={newTitle} onChange={setNewTitle} placeholder="Aufgabentitel" required />
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel htmlFor="task-desc">Beschreibung</FormLabel>
|
||||
<FormTextarea id="task-desc" value={newDesc} onChange={setNewDesc} placeholder="Beschreibung der Aufgabe..." />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<FormLabel htmlFor="task-cat">Kategorie</FormLabel>
|
||||
<FormSelect id="task-cat" value={newCategory} onChange={setNewCategory}
|
||||
options={TASK_CATEGORIES.map((c) => ({ value: c, label: c }))} />
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel htmlFor="task-prio">Prioritaet</FormLabel>
|
||||
<FormSelect id="task-prio" value={newPriority} onChange={setNewPriority}
|
||||
options={[
|
||||
{ value: 'urgent', label: 'Dringend' }, { value: 'high', label: 'Hoch' },
|
||||
{ value: 'medium', label: 'Mittel' }, { value: 'low', label: 'Niedrig' },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel htmlFor="task-due">Faelligkeitsdatum</FormLabel>
|
||||
<FormInput id="task-due" type="date" value={newDueDate} onChange={setNewDueDate} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<SecondaryButton onClick={() => setShowModal(false)}>Abbrechen</SecondaryButton>
|
||||
<PrimaryButton type="submit" disabled={saving || !newTitle.trim()}>
|
||||
{saving ? 'Erstelle...' : 'Aufgabe erstellen'}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user