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>
127 lines
5.2 KiB
TypeScript
127 lines
5.2 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { AssignmentOverview, ASSIGNMENT_STATUS_LABELS, ASSIGNMENT_STATUS_COLORS, formatDate } from './types'
|
|
import {
|
|
Badge, ComplianceBar, HoursBar,
|
|
IconBack, IconTask, IconClock, IconMail, IconSettings, IconShield,
|
|
} from './ui-primitives'
|
|
import { AufgabenTab } from './AufgabenTab'
|
|
import { ZeiterfassungTab } from './ZeiterfassungTab'
|
|
import { KommunikationTab } from './KommunikationTab'
|
|
import { EinstellungenTab } from './EinstellungenTab'
|
|
|
|
type DetailTab = 'aufgaben' | 'zeit' | 'kommunikation' | 'einstellungen'
|
|
|
|
export function DetailView({
|
|
assignment,
|
|
onBack,
|
|
onUpdate,
|
|
addToast,
|
|
}: {
|
|
assignment: AssignmentOverview
|
|
onBack: () => void
|
|
onUpdate: () => void
|
|
addToast: (msg: string, type?: 'success' | 'error') => void
|
|
}) {
|
|
const [activeTab, setActiveTab] = useState<DetailTab>('aufgaben')
|
|
|
|
const tabs: { id: DetailTab; label: string; icon: React.ReactNode }[] = [
|
|
{ id: 'aufgaben', label: 'Aufgaben', icon: <IconTask className="w-4 h-4" /> },
|
|
{ id: 'zeit', label: 'Zeiterfassung', icon: <IconClock className="w-4 h-4" /> },
|
|
{ id: 'kommunikation', label: 'Kommunikation', icon: <IconMail className="w-4 h-4" /> },
|
|
{ id: 'einstellungen', label: 'Einstellungen', icon: <IconSettings className="w-4 h-4" /> },
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Back + Header */}
|
|
<div>
|
|
<button onClick={onBack}
|
|
className="flex items-center gap-2 text-sm text-purple-600 hover:text-purple-800 font-medium mb-4 transition-colors">
|
|
<IconBack className="w-4 h-4" /> Zurueck zur Uebersicht
|
|
</button>
|
|
|
|
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center text-purple-600">
|
|
<IconShield className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-900">{assignment.tenant_name}</h2>
|
|
<p className="text-sm text-gray-400 font-mono">{assignment.tenant_slug}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
label={ASSIGNMENT_STATUS_LABELS[assignment.status] || assignment.status}
|
|
className={ASSIGNMENT_STATUS_COLORS[assignment.status] || 'bg-gray-100 text-gray-600'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Meta info */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-5 pt-5 border-t border-gray-100">
|
|
<div>
|
|
<p className="text-xs text-gray-400">Vertragsbeginn</p>
|
|
<p className="text-sm font-medium text-gray-700">{formatDate(assignment.contract_start)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-400">Vertragsende</p>
|
|
<p className="text-sm font-medium text-gray-700">
|
|
{assignment.contract_end ? formatDate(assignment.contract_end) : 'Unbefristet'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-400">Compliance-Score</p>
|
|
<div className="mt-1"><ComplianceBar score={assignment.compliance_score} /></div>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-400">Stunden diesen Monat</p>
|
|
<div className="mt-1"><HoursBar used={assignment.hours_this_month} budget={assignment.hours_budget} /></div>
|
|
</div>
|
|
</div>
|
|
|
|
{assignment.notes && (
|
|
<div className="mt-4 pt-4 border-t border-gray-100">
|
|
<p className="text-xs text-gray-400 mb-1">Anmerkungen</p>
|
|
<p className="text-sm text-gray-600">{assignment.notes}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="border-b border-gray-200">
|
|
<nav className="flex gap-0 -mb-px overflow-x-auto">
|
|
{tabs.map((tab) => (
|
|
<button key={tab.id} onClick={() => setActiveTab(tab.id)}
|
|
className={`flex items-center gap-2 px-5 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
|
|
activeTab === tab.id
|
|
? 'border-purple-600 text-purple-700'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}>
|
|
{tab.icon} {tab.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Tab content */}
|
|
<div>
|
|
{activeTab === 'aufgaben' && <AufgabenTab assignmentId={assignment.id} addToast={addToast} />}
|
|
{activeTab === 'zeit' && (
|
|
<ZeiterfassungTab assignmentId={assignment.id} monthlyBudget={assignment.monthly_hours_budget} addToast={addToast} />
|
|
)}
|
|
{activeTab === 'kommunikation' && <KommunikationTab assignmentId={assignment.id} addToast={addToast} />}
|
|
{activeTab === 'einstellungen' && (
|
|
<EinstellungenTab assignment={assignment} onUpdate={onUpdate} addToast={addToast} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|