Sichert den lokalen Entwicklungsstand nach Gitea: Nav/Dashboard, Personen-Excel/Archiv, Team-Setup, Assignment-Übergabe und C6/C7/T2. Co-authored-by: Cursor <cursoragent@cursor.com>
286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { useNavigate, useParams } from 'react-router-dom'
|
||
import {
|
||
type Assignment, type AssignmentType, type Consultant,
|
||
getAssignment, updateAssignment, listAssignmentTypes, listConsultants,
|
||
} from '../db'
|
||
|
||
export default function AssignmentEdit() {
|
||
const { id } = useParams()
|
||
const navigate = useNavigate()
|
||
const assignmentId = Number(id)
|
||
|
||
const [assignment, setAssignment] = useState<Assignment | null>(null)
|
||
const [assignmentTypes, setAssignmentTypes] = useState<AssignmentType[]>([])
|
||
const [allConsultants, setAllConsultants] = useState<Consultant[]>([])
|
||
|
||
// Form fields
|
||
const [assignmentNumber, setAssignmentNumber] = useState('')
|
||
const [title, setTitle] = useState('')
|
||
const [client, setClient] = useState('')
|
||
const [description, setDescription] = useState('')
|
||
const [assignmentTypeId, setAssignmentTypeId] = useState(0)
|
||
const [leadName, setLeadName] = useState('')
|
||
const [status, setStatus] = useState<'active' | 'done'>('active')
|
||
const [instituteeIds, setInstituteeIds] = useState<number[]>([])
|
||
const [instituteeEnrollments, setInstituteeEnrollments] = useState<Array<{ instituteeId: number; from?: string; to?: string }>>([])
|
||
const [plannedStart, setPlannedStart] = useState('')
|
||
const [plannedEnd, setPlannedEnd] = useState('')
|
||
const [typeChanged, setTypeChanged] = useState(false)
|
||
const [addInstituteeId, setAddInstituteeId] = useState(0)
|
||
|
||
useEffect(() => {
|
||
const loadData = async () => {
|
||
const [a, types, cons] = await Promise.all([
|
||
getAssignment(assignmentId),
|
||
listAssignmentTypes(),
|
||
listConsultants(),
|
||
])
|
||
if (!a) return
|
||
setAssignment(a)
|
||
setAssignmentTypes(types)
|
||
setAllConsultants(cons)
|
||
|
||
setAssignmentNumber(a.assignmentNumber ?? '')
|
||
setTitle(a.title)
|
||
setClient(a.client)
|
||
setDescription(a.description ?? '')
|
||
setAssignmentTypeId(a.assignmentTypeId)
|
||
setLeadName(a.leadName)
|
||
setStatus(a.status)
|
||
setInstituteeIds(a.instituteeIds ?? [])
|
||
setInstituteeEnrollments(a.instituteeEnrollments ?? [])
|
||
setPlannedStart(a.plannedStart?.slice(0, 10) ?? '')
|
||
setPlannedEnd(a.plannedEnd?.slice(0, 10) ?? '')
|
||
}
|
||
loadData()
|
||
}, [assignmentId])
|
||
|
||
const handleSave = async () => {
|
||
await updateAssignment(assignmentId, {
|
||
assignmentNumber: assignmentNumber.trim() || undefined,
|
||
title,
|
||
client,
|
||
description: description.trim() || undefined,
|
||
assignmentTypeId,
|
||
leadName,
|
||
status,
|
||
instituteeIds,
|
||
instituteeEnrollments,
|
||
plannedStart: plannedStart || undefined,
|
||
plannedEnd: plannedEnd || undefined,
|
||
})
|
||
navigate(`/assignment/${assignmentId}`)
|
||
}
|
||
|
||
const handleTypeChange = (newTypeId: number) => {
|
||
if (newTypeId !== assignmentTypeId) setTypeChanged(true)
|
||
setAssignmentTypeId(newTypeId)
|
||
}
|
||
|
||
const addInstitutee = () => {
|
||
if (!addInstituteeId || instituteeIds.includes(addInstituteeId)) return
|
||
setInstituteeIds(prev => [...prev, addInstituteeId])
|
||
setInstituteeEnrollments(prev => [...prev, { instituteeId: addInstituteeId, from: '', to: '' }])
|
||
setAddInstituteeId(0)
|
||
}
|
||
|
||
const removeInstitutee = (cid: number) => {
|
||
setInstituteeIds(prev => prev.filter(id => id !== cid))
|
||
setInstituteeEnrollments(prev => prev.filter(e => e.instituteeId !== cid))
|
||
}
|
||
|
||
const updateEnrollment = (cid: number, field: 'from' | 'to', value: string) => {
|
||
setInstituteeEnrollments(prev =>
|
||
prev.map(e => e.instituteeId === cid ? { ...e, [field]: value } : e)
|
||
)
|
||
}
|
||
|
||
const availableConsultants = allConsultants.filter(c => !instituteeIds.includes(c.id!) && !c.archivedAt)
|
||
|
||
if (!assignment) return <div className="text-center py-10 text-gray-400">Lädt…</div>
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* Header */}
|
||
<div>
|
||
<button onClick={() => navigate(`/assignment/${assignmentId}`)}
|
||
className="text-sm text-brand mb-2 block">← Zurück</button>
|
||
<h1 className="text-xl font-bold text-gray-800">Assignment bearbeiten</h1>
|
||
</div>
|
||
|
||
{/* Formular */}
|
||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-4 space-y-3">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Assignment-Nummer</label>
|
||
<input
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
placeholder="z. B. CGI-2024-001"
|
||
value={assignmentNumber}
|
||
onChange={e => setAssignmentNumber(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Titel</label>
|
||
<input
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={title}
|
||
onChange={e => setTitle(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Kunde</label>
|
||
<input
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={client}
|
||
onChange={e => setClient(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Beschreibung</label>
|
||
<textarea
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
rows={3}
|
||
value={description}
|
||
onChange={e => setDescription(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Geplanter Beginn</label>
|
||
<input
|
||
type="date"
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={plannedStart}
|
||
onChange={e => setPlannedStart(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Geplantes Ende</label>
|
||
<input
|
||
type="date"
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={plannedEnd}
|
||
onChange={e => setPlannedEnd(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Assignment-Typ</label>
|
||
<select
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={assignmentTypeId}
|
||
onChange={e => handleTypeChange(Number(e.target.value))}>
|
||
{assignmentTypes.map(t => (
|
||
<option key={t.id} value={t.id}>{t.name}</option>
|
||
))}
|
||
</select>
|
||
{typeChanged && (
|
||
<div className="text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mt-1">
|
||
⚠️ Bestehende Meetings bleiben erhalten
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Lead</label>
|
||
<input
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={leadName}
|
||
onChange={e => setLeadName(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-gray-500">Status</label>
|
||
<select
|
||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={status}
|
||
onChange={e => setStatus(e.target.value as 'active' | 'done')}>
|
||
<option value="active">Aktiv</option>
|
||
<option value="done">Abgeschlossen</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Institutees */}
|
||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-4 space-y-3">
|
||
<h2 className="font-semibold text-gray-800">Institutees</h2>
|
||
|
||
{instituteeIds.length === 0 && (
|
||
<p className="text-sm text-gray-400">Noch keine Institutees zugewiesen.</p>
|
||
)}
|
||
|
||
{instituteeIds.map(cid => {
|
||
const consultant = allConsultants.find(c => c.id === cid)
|
||
const enrollment = instituteeEnrollments.find(e => e.instituteeId === cid)
|
||
if (!consultant) return null
|
||
return (
|
||
<div key={cid} className="flex items-center gap-2 flex-wrap border border-gray-100 rounded-lg px-3 py-2">
|
||
<span className="text-sm font-medium text-gray-700 flex-1 min-w-[100px]">
|
||
{consultant.firstName} {consultant.lastName}
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-xs text-gray-400">Von</span>
|
||
<input
|
||
type="date"
|
||
className="border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-brand"
|
||
value={enrollment?.from ?? ''}
|
||
onChange={e => updateEnrollment(cid, 'from', e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-xs text-gray-400">Bis</span>
|
||
<input
|
||
type="date"
|
||
className="border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-brand"
|
||
value={enrollment?.to ?? ''}
|
||
onChange={e => updateEnrollment(cid, 'to', e.target.value)}
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={() => removeInstitutee(cid)}
|
||
className="text-red-300 hover:text-red-500 text-sm px-1">
|
||
×
|
||
</button>
|
||
</div>
|
||
)
|
||
})}
|
||
|
||
{/* Institutee hinzufügen */}
|
||
{availableConsultants.length > 0 && (
|
||
<div className="flex gap-2">
|
||
<select
|
||
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
||
value={addInstituteeId}
|
||
onChange={e => setAddInstituteeId(Number(e.target.value))}>
|
||
<option value={0}>Institutee auswählen…</option>
|
||
{availableConsultants.map(c => (
|
||
<option key={c.id} value={c.id}>{c.firstName} {c.lastName}</option>
|
||
))}
|
||
</select>
|
||
<button
|
||
onClick={addInstitutee}
|
||
disabled={!addInstituteeId}
|
||
className="px-4 py-2 bg-brand text-white text-sm rounded-lg font-medium disabled:opacity-40">
|
||
Hinzufügen
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Aktionen */}
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={handleSave}
|
||
className="flex-1 py-3 bg-brand text-white rounded-xl text-sm font-semibold">
|
||
Speichern
|
||
</button>
|
||
<button
|
||
onClick={() => navigate(`/assignment/${assignmentId}`)}
|
||
className="px-5 py-3 border border-gray-200 text-gray-500 rounded-xl text-sm">
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|