fix: Evaluation.tsx nach Assignment gruppiert, Papierkorb/Status gefiltert

- Oberste Gliederungsebene ist jetzt das Assignment, Institutees darunter
- Meeting-Filter: nur status='done', keine Papierkorb-Meetings (deletedAt)
- Migriert auf feedbackCriterionItems statt Legacy categories/criteria
- zustand aus package.json entfernt (ungenutzt)
This commit is contained in:
Lars 2026-07-02 11:16:41 +02:00
parent 63ca6583fa
commit f57db9d161

View File

@ -75,7 +75,7 @@ export default function Evaluation() {
for (const a of assignments) {
const meetingIds = (await db.meetingInstances
.where('assignmentId').equals(a.id!).toArray()
).map(m => m.id!)
).filter(m => !m.deletedAt && m.status === 'done').map(m => m.id!)
for (const instId of a.instituteeIds) {
const inst = consultants.find(c => c.id === instId)
@ -87,7 +87,7 @@ export default function Evaluation() {
if (instAssessments.length === 0) continue
const getScore = (itemId: number): FeedbackRating | null => {
const relevant = instAssessments.filter(a => a.criteriaId === itemId && a.score !== null)
const relevant = instAssessments.filter(a => a.criteriaId === itemId && a.score !== null && a.score !== 'na')
if (relevant.length === 0) return null
const avg = relevant.reduce((s, a) => s + RATING_NUM_MAP[a.score!], 0) / relevant.length
if (avg < 1.5) return 'not_client_ready'
@ -114,62 +114,82 @@ export default function Evaluation() {
const rowKey = (r: InstScore) => `${r.assignment.id}-${r.institutee.id}`
const assignmentGroups = data.reduce<Map<number, { assignment: Assignment; rows: InstScore[] }>>(
(map, row) => {
const key = row.assignment.id!
const group = map.get(key)
if (group) group.rows.push(row)
else map.set(key, { assignment: row.assignment, rows: [row] })
return map
},
new Map()
)
return (
<div className="space-y-4">
<div className="space-y-6">
<h1 className="text-xl font-bold text-gray-800">Auswertung</h1>
{data.length === 0 && (
<p className="text-gray-500 text-sm">Noch keine Bewertungen vorhanden.</p>
)}
{data.map(row => (
<div key={rowKey(row)} className="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<div
className="flex items-start justify-between cursor-pointer"
onClick={() => setSelected(selected === rowKey(row) ? null : rowKey(row))}>
<div>
<div className="font-semibold text-gray-800">
{row.institutee.firstName} {row.institutee.lastName}
</div>
<div className="text-xs text-gray-400">{row.assignment.title} · {row.assignment.client}</div>
</div>
<div className="flex items-center gap-2">
<div className={`px-3 py-1 rounded-full text-sm font-bold ${ratingColor(row.overall)}`}>
{ratingLabel(row.overall)}
</div>
<button
onClick={e => { e.stopPropagation(); navigate(`/assignment/${row.assignment.id}/feedback/${row.institutee.id}`) }}
className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-lg">
Feedback
</button>
</div>
{[...assignmentGroups.values()].map(({ assignment, rows }) => (
<div key={assignment.id} className="space-y-2">
<div className="px-1">
{assignment.assignmentNumber && (
<div className="text-xs font-semibold text-brand uppercase tracking-wide">{assignment.assignmentNumber}</div>
)}
<h2 className="font-bold text-gray-800">{assignment.title}</h2>
<div className="text-xs text-gray-400">{assignment.client}</div>
</div>
{selected === rowKey(row) && (
<div className="mt-4 space-y-4">
{dimensions.map(dim => {
const dimCats = categories.filter(c => c.dimensionId === dim.id)
const dimScores = row.catScores.filter(cs =>
dimCats.some(c => c.id === cs.category.id) && cs.avg !== null
)
if (dimScores.length === 0) return null
return (
<div key={dim.id}>
<div className="text-xs font-semibold uppercase text-gray-400 tracking-wide mb-2">
{dim.name}
</div>
{dimScores.map(({ category, avg }) => (
<div key={category.id}
className="flex items-center justify-between py-1.5 border-b border-gray-50 last:border-0">
<span className="text-sm text-gray-700">{category.name}</span>
<span className={`text-xs font-semibold px-2 py-0.5 rounded ${ratingColor(avg)}`}>
{ratingLabel(avg)}
</span>
</div>
))}
{rows.map(row => (
<div key={rowKey(row)} className="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<div
className="flex items-start justify-between cursor-pointer"
onClick={() => setSelected(selected === rowKey(row) ? null : rowKey(row))}>
<div className="font-semibold text-gray-800">
{row.institutee.firstName} {row.institutee.lastName}
</div>
<div className="flex items-center gap-2">
<div className={`px-3 py-1 rounded-full text-sm font-bold ${ratingColor(row.overall)}`}>
{ratingLabel(row.overall)}
</div>
)
})}
<button
onClick={e => { e.stopPropagation(); navigate(`/assignment/${row.assignment.id}/feedback/${row.institutee.id}`) }}
className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-lg">
Feedback
</button>
</div>
</div>
{selected === rowKey(row) && (
<div className="mt-4 space-y-4">
{dimensions.map(dim => {
const dimCats = categories.filter(c => c.dimensionId === dim.id)
const dimScores = row.catScores.filter(cs =>
dimCats.some(c => c.id === cs.category.id) && cs.avg !== null
)
if (dimScores.length === 0) return null
return (
<div key={dim.id}>
<div className="text-xs font-semibold uppercase text-gray-400 tracking-wide mb-2">
{dim.name}
</div>
{dimScores.map(({ category, avg }) => (
<div key={category.id}
className="flex items-center justify-between py-1.5 border-b border-gray-50 last:border-0">
<span className="text-sm text-gray-700">{category.name}</span>
<span className={`text-xs font-semibold px-2 py-0.5 rounded ${ratingColor(avg)}`}>
{ratingLabel(avg)}
</span>
</div>
))}
</div>
)
})}
</div>
)}
</div>
)}
))}
</div>
))}
</div>