CG-Feedback-Monitor/src/pages/AssignmentEdit.tsx
Lars d894c3c8e4 V2-Stand: UX-Dashboard, Personen v2, Starter-Paket und Dexie-Legacy entfernt.
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>
2026-09-16 14:17:25 +02:00

286 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}