import { useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { type Assignment, type AssignmentType, type PhaseTemplate, type MeetingInstance, type Consultant, type Assessment, type RatingScore, type FeedbackCategory, type FeedbackCriterionItem, getAssignment, getAssignmentType, listPhaseTemplatesForType, listMeetingInstancesForAssignment, addMeetingInstance, softDeleteMeetingInstance, restoreMeetingInstance, hardDeleteMeetingInstance, getConsultantsByIds, listAssessmentsForMeetings, listMeetingCategoryRatingsForMeetings, loadFeedbackStructure, resolveVisibleStructure, isAssignmentFeedbackComplete, softDeleteAssignment, archiveAssignment, } from '../db' import { exportMeetingAsMarkdown, exportMeetingAsJson, downloadFile } from '../utils/meetingExport' import { exportAssignmentReportPdf, exportInstituteeFeedbackPdf } from '../utils/pdfExport' import { resolveScoreStyle } from '../config/constants' import { resolveCategoryChipScore } from '../utils/meetingCategoryScore' import { useRatingColorBands } from '../hooks/useRatingColorBands' import type { MeetingCategoryRating } from '../db' interface PhaseRow { template: PhaseTemplate instances: MeetingInstance[] } export default function AssignmentDetail() { const { id } = useParams() const navigate = useNavigate() const assignmentId = Number(id) const [assignment, setAssignment] = useState(null) const [assignmentType, setAssignmentType] = useState(null) const [phaseRows, setPhaseRows] = useState([]) const [allTemplates, setAllTemplates] = useState([]) const [institutees, setInstitutees] = useState([]) const [deletedInstances, setDeletedInstances] = useState([]) const [showAdHoc, setShowAdHoc] = useState(false) const [showTrash, setShowTrash] = useState(false) const [exportMenuId, setExportMenuId] = useState(null) const [confirmSoftDelete, setConfirmSoftDelete] = useState(null) const [confirmHardDelete, setConfirmHardDelete] = useState(null) const [allAssessments, setAllAssessments] = useState([]) const [categoryRatings, setCategoryRatings] = useState([]) const [fbCategories, setFbCategories] = useState([]) const [fbItems, setFbItems] = useState([]) const colorBands = useRatingColorBands() const [feedbackComplete, setFeedbackComplete] = useState(false) const [confirmAssignmentTrash, setConfirmAssignmentTrash] = useState(false) const [confirmAssignmentArchive, setConfirmAssignmentArchive] = useState(false) const [pdfExportingKey, setPdfExportingKey] = useState(null) const load = async () => { const a = await getAssignment(assignmentId) if (!a) return setAssignment(a) setFeedbackComplete(a.status === 'done' ? await isAssignmentFeedbackComplete(assignmentId) : false) const [aType, templates, instances, cons] = await Promise.all([ getAssignmentType(a.assignmentTypeId), listPhaseTemplatesForType(a.assignmentTypeId), listMeetingInstancesForAssignment(assignmentId), getConsultantsByIds(a.instituteeIds), ]) setAssignmentType(aType ?? null) setAllTemplates(templates) setInstitutees(a.instituteeIds.map(cid => cons.find(c => c.id === cid)!).filter(Boolean)) const active = instances.filter(i => !i.deletedAt) const deleted = instances.filter(i => !!i.deletedAt) setDeletedInstances(deleted) setPhaseRows(templates.map(t => ({ template: t, instances: active.filter(i => i.phaseTemplateId === t.id).sort((a, b) => a.phaseIndex - b.phaseIndex), }))) const instIds = active.map(i => i.id!) const [asmts, catRatings, structure, aType2] = await Promise.all([ listAssessmentsForMeetings(instIds), listMeetingCategoryRatingsForMeetings(instIds), loadFeedbackStructure(), getAssignmentType(a.assignmentTypeId), ]) setAllAssessments(asmts) setCategoryRatings(catRatings) const visible = resolveVisibleStructure(structure.dimensions, structure.categories, structure.items, aType2?.criteriaIds) setFbItems(visible.items) setFbCategories(visible.categories) } useEffect(() => { load() }, [assignmentId]) const startMeeting = async (templateId: number, existingCount: number) => { const mid = await addMeetingInstance({ assignmentId, phaseTemplateId: templateId, phaseIndex: existingCount + 1, date: new Date().toISOString().slice(0, 10), status: 'active', generalNotes: '', }) navigate(`/assignment/${assignmentId}/meeting/${mid}`) } const buildFilename = (inst: MeetingInstance, ext: string) => { const tmpl = allTemplates.find(t => t.id === inst.phaseTemplateId) const date = new Date(inst.date).toLocaleDateString('de-DE').replace(/\./g, '-') const label = tmpl?.label ?? 'Meeting' const idx = inst.phaseIndex > 1 ? `-${inst.phaseIndex}` : '' const id = assignment?.assignmentNumber ?? assignment?.title ?? 'export' return `${id}_${date}_${label}${idx}.${ext}` } const handleExportMd = async (inst: MeetingInstance) => { const md = await exportMeetingAsMarkdown(inst.id!) downloadFile(md, buildFilename(inst, 'md'), 'text/markdown') setExportMenuId(null) } const handleExportJson = async (inst: MeetingInstance) => { const json = await exportMeetingAsJson(inst.id!) downloadFile(json, buildFilename(inst, 'json'), 'application/json') setExportMenuId(null) } const softDeleteMeeting = async (instId: number) => { await softDeleteMeetingInstance(instId) setConfirmSoftDelete(null) await load() } const restoreMeeting = async (instId: number) => { await restoreMeetingInstance(instId) await load() } const hardDeleteMeeting = async (instId: number) => { await hardDeleteMeetingInstance(instId) setConfirmHardDelete(null) await load() } const trashAssignment = async () => { await softDeleteAssignment(assignmentId) navigate('/') } const archiveThisAssignment = async () => { await archiveAssignment(assignmentId) navigate('/') } const handleExportReportPdf = async () => { setPdfExportingKey('report') try { await exportAssignmentReportPdf(assignmentId) } finally { setPdfExportingKey(null) } } const handleExportInstituteeFeedbackPdf = async (instituteeId: number) => { const key = `inst-${instituteeId}` setPdfExportingKey(key) try { await exportInstituteeFeedbackPdf(assignmentId, instituteeId) } finally { setPdfExportingKey(null) } } const statusBadge = (status: MeetingInstance['status']) => { const map = { planned: 'bg-yellow-100 text-yellow-700', active: 'bg-green-100 text-green-700', done: 'bg-gray-100 text-gray-500', } const label = { planned: 'Geplant', active: 'Aktiv', done: 'Fertig' } return {label[status]} } if (!assignment) return
Lädt…
return (
{assignment.assignmentNumber && (
{assignment.assignmentNumber}
)}

{assignment.title}

{assignment.client} · {assignmentType?.name}
{institutees.map(c => `${c.firstName} ${c.lastName}`).join(', ')}
{/* Assignment-Aktionen (nur für abgeschlossene Assignments) */} {assignment.status === 'done' && (
Assignment-Aktionen
{!feedbackComplete && (
Löschen erst möglich, sobald für alle Institutees ein finales Feedback vorliegt.
)} {confirmAssignmentTrash && (
Assignment in den Papierkorb verschieben?
)} {confirmAssignmentArchive && (
Assignment archivieren?
)}
)} {/* Ad-hoc Meeting */}
Ad-hoc Meeting starten
{showAdHoc && (
{allTemplates.map(t => ( ))}
)}
{/* Feedback-Entwürfe */} {institutees.length > 0 && (
Feedback-Entwürfe
{institutees.map(c => (
))}
)} {/* Phasen-Flow */}
setExportMenuId(null)}> {phaseRows.map(({ template, instances }) => { const canStart = instances.length === 0 || template.isRepeatable return (
{template.label} {template.withWhom === 'client' ? 'Kunde' : 'Lead'} {template.hasConversation && ( Protokoll )}
{instances.map(inst => (
{ if (confirmSoftDelete !== inst.id) navigate(`/assignment/${assignmentId}/meeting/${inst.id}`) }} className="flex items-start justify-between py-2 px-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 gap-2">
{template.isRepeatable ? `#${inst.phaseIndex} · ` : ''} {new Date(inst.date).toLocaleDateString('de-DE')}
{/* Per-institutee per-category chips */} {institutees.map(c => { const chips = fbCategories.map(cat => { const catItems = fbItems.filter(i => i.categoryId === cat.id) const mode = resolveCategoryChipScore({ meetingId: inst.id!, instituteeId: c.id!, categoryId: cat.id!, categoryRatings, assessments: allAssessments, catItems, }) return mode ? { cat, mode } : null }).filter(Boolean) as { cat: FeedbackCategory; mode: RatingScore }[] if (chips.length === 0) return null return (
{c.firstName}: {chips.map(({ cat, mode }) => ( {cat.name} {mode} ))}
) })}
{statusBadge(inst.status)}
{exportMenuId === inst.id && (
)} {confirmSoftDelete === inst.id && (
In Papierkorb verschieben?
)}
))}
) })}
{/* Papierkorb */} {deletedInstances.length > 0 && (
{showTrash && (
{deletedInstances.map(inst => { const tmpl = allTemplates.find(t => t.id === inst.phaseTemplateId) return (
{tmpl?.label ?? '—'}
{new Date(inst.date).toLocaleDateString('de-DE')} {inst.deletedAt && ` · gelöscht ${new Date(inst.deletedAt).toLocaleDateString('de-DE')}`}
{confirmHardDelete === inst.id && (
Endgültig löschen? Alle Daten dieses Meetings werden unwiderruflich entfernt.
)}
) })}
)}
)}
) }