From 63ca6583fa746051e0b7032efeca83a176b369f2 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 2 Jul 2026 10:58:44 +0200 Subject: [PATCH] refactor: Evaluation.tsx auf feedbackCriterionItems migriert, zustand entfernt - Liest jetzt feedbackDimensions/feedbackCategories/feedbackCriterionItems statt alter categories/criteria-Tabellen - Korrekte 1-4 Rating-Skala mit RATING_OPTIONS-Labels - Aggregation via RATING_NUM_MAP aus constants.ts - zustand aus package.json entfernt (war nicht genutzt) --- package.json | 1 - src/pages/Evaluation.tsx | 143 +++++++++++++++++++++++++-------------- 2 files changed, 92 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 18b1dc7..8be877d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "react-dom": "^19.0.0", "react-router-dom": "^7.6.2", "dexie": "^4.0.11", - "zustand": "^5.0.5", "recharts": "^2.15.3" }, "devDependencies": { diff --git a/src/pages/Evaluation.tsx b/src/pages/Evaluation.tsx index 477e418..9d63285 100644 --- a/src/pages/Evaluation.tsx +++ b/src/pages/Evaluation.tsx @@ -1,67 +1,110 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { db, type Assignment, type Consultant, type Category, type Criterion, type FeedbackRating } from '../db' +import { + db, + type Assignment, type Consultant, + type FeedbackDimension, type FeedbackCategory, type FeedbackCriterionItem, + type FeedbackRating, +} from '../db' +import { RATING_OPTIONS, RATING_NUM_MAP } from '../config/constants' -const ratingToNum = (r: FeedbackRating | null | undefined): number => { - const m: Record = { na: 0, not_client_ready: 1, partially_client_ready: 2, nearly_client_ready: 3, client_ready: 4 } - return r ? (m[r] ?? 0) : 0 +interface CatScore { + category: FeedbackCategory + avg: number | null } interface InstScore { institutee: Consultant assignment: Assignment - scores: { criterion: Criterion; avg: number | null }[] - weightedTotal: number | null + catScores: CatScore[] + overall: number | null +} + +function weightedAvg( + items: FeedbackCriterionItem[], + getScore: (itemId: number) => FeedbackRating | null, +): number | null { + const rated = items + .map(i => ({ score: getScore(i.id!), w: i.weight ?? 1 })) + .filter((x): x is { score: FeedbackRating; w: number } => x.score !== null && x.score !== 'na') + if (rated.length === 0) return null + const wSum = rated.reduce((s, x) => s + x.w, 0) + return rated.reduce((s, x) => s + RATING_NUM_MAP[x.score] * x.w, 0) / wSum +} + +function ratingColor(avg: number | null): string { + if (avg === null) return 'bg-gray-100 text-gray-400' + if (avg >= 3.5) return 'bg-green-100 text-green-700' + if (avg >= 2.5) return 'bg-lime-100 text-lime-700' + if (avg >= 1.5) return 'bg-orange-100 text-orange-700' + return 'bg-red-100 text-red-700' +} + +function ratingLabel(avg: number | null): string { + if (avg === null) return 'โ€”' + const r = RATING_OPTIONS.find(o => + avg >= 3.5 ? o.value === 'client_ready' : + avg >= 2.5 ? o.value === 'nearly_client_ready' : + avg >= 1.5 ? o.value === 'partially_client_ready' : + o.value === 'not_client_ready' + ) + return r ? `${r.short} (${avg.toFixed(1)})` : avg.toFixed(1) } export default function Evaluation() { - const [data, setData] = useState([]) - const [categories, setCategories] = useState([]) - const [selected, setSelected] = useState(null) + const [data, setData] = useState([]) + const [dimensions, setDimensions] = useState([]) + const [categories, setCategories] = useState([]) + const [selected, setSelected] = useState(null) const navigate = useNavigate() useEffect(() => { const load = async () => { - const [assignments, consultants, criteria, cats, assessments] = await Promise.all([ + const [assignments, consultants, dims, cats, items, assessments] = await Promise.all([ db.assignments.toArray(), db.consultants.toArray(), - db.criteria.orderBy('order').toArray(), - db.categories.orderBy('order').toArray(), + db.feedbackDimensions.orderBy('order').toArray(), + db.feedbackCategories.orderBy('order').toArray(), + db.feedbackCriterionItems.orderBy('order').toArray(), db.assessments.toArray(), ]) + setDimensions(dims) setCategories(cats) const rows: InstScore[] = [] for (const a of assignments) { + const meetingIds = (await db.meetingInstances + .where('assignmentId').equals(a.id!).toArray() + ).map(m => m.id!) + for (const instId of a.instituteeIds) { const inst = consultants.find(c => c.id === instId) if (!inst) continue - // Nur Meetings dieses Assignments - const meetingIds = (await db.meetingInstances.where('assignmentId').equals(a.id!).toArray()).map(m => m.id!) const instAssessments = assessments.filter( as => as.instituteeId === instId && meetingIds.includes(as.meetingInstanceId) ) if (instAssessments.length === 0) continue - const usedCritIds = [...new Set(instAssessments.map(a => a.criteriaId))] - const usedCrit = criteria.filter(c => usedCritIds.includes(c.id!)) + const getScore = (itemId: number): FeedbackRating | null => { + const relevant = instAssessments.filter(a => a.criteriaId === itemId && a.score !== null) + 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' + if (avg < 2.5) return 'partially_client_ready' + if (avg < 3.5) return 'nearly_client_ready' + return 'client_ready' + } - const scores = usedCrit.map(crit => { - const relevant = instAssessments.filter(a => a.criteriaId === crit.id && a.score !== null) - const avg = relevant.length > 0 - ? relevant.reduce((s, a) => s + ratingToNum(a.score), 0) / relevant.length - : null - return { criterion: crit, avg } - }) + const catScores: CatScore[] = cats.map(cat => ({ + category: cat, + avg: weightedAvg(items.filter(i => i.categoryId === cat.id), getScore), + })) - const rated = scores.filter(s => s.avg !== null) - const weightedTotal = rated.length > 0 - ? rated.reduce((s, x) => s + x.avg! * x.criterion.weight, 0) / - rated.reduce((s, x) => s + x.criterion.weight, 0) - : null + const overall = weightedAvg(items, getScore) + if (overall === null && catScores.every(c => c.avg === null)) continue - rows.push({ institutee: inst, assignment: a, scores, weightedTotal }) + rows.push({ institutee: inst, assignment: a, catScores, overall }) } } setData(rows) @@ -69,13 +112,6 @@ export default function Evaluation() { load() }, []) - const scoreColor = (avg: number | null) => { - if (avg === null) return 'bg-gray-100 text-gray-400' - if (avg >= 4) return 'bg-green-100 text-green-700' - if (avg >= 3) return 'bg-yellow-100 text-yellow-700' - return 'bg-red-100 text-red-700' - } - const rowKey = (r: InstScore) => `${r.assignment.id}-${r.institutee.id}` return ( @@ -88,8 +124,7 @@ export default function Evaluation() {
setSelected(selected === rowKey(row) ? null : rowKey(row))} - > + onClick={() => setSelected(selected === rowKey(row) ? null : rowKey(row))}>
{row.institutee.firstName} {row.institutee.lastName} @@ -97,8 +132,8 @@ export default function Evaluation() {
{row.assignment.title} ยท {row.assignment.client}
-
- {row.weightedTotal !== null ? row.weightedTotal.toFixed(1) : 'โ€”'} / 5 +
+ {ratingLabel(row.overall)}
{selected === rowKey(row) && ( -
- {categories.map(cat => { - const catScores = row.scores.filter(s => s.criterion.categoryId === cat.id) - if (catScores.every(s => s.avg === null)) return null +
+ {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 ( -
-
{cat.name}
- {catScores.map(({ criterion, avg }) => ( -
- {criterion.name} - - {avg !== null ? avg.toFixed(1) : 'โ€”'} +
+
+ {dim.name} +
+ {dimScores.map(({ category, avg }) => ( +
+ {category.name} + + {ratingLabel(avg)}
))}