[split-required] Split final batch of monoliths >1000 LOC

Python (6 files in klausur-service):
- rbac.py (1,132 → 4), admin_api.py (1,012 → 4)
- routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5)

Python (2 files in backend-lehrer):
- unit_api.py (1,226 → 6), game_api.py (1,129 → 5)

Website (6 page files):
- 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components
  in website/components/klausur-korrektur/ (17 shared files)
- companion (1,057 → 10), magic-help (1,017 → 8)

All re-export barrels preserve backward compatibility.
Zero import errors verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 23:17:30 +02:00
parent b2a0126f14
commit 6811264756
67 changed files with 12270 additions and 13651 deletions

View File

@@ -0,0 +1,120 @@
'use client'
/**
* Criteria scoring tab content.
* Shows sliders and annotation counts for each grading criterion.
*/
import type { Annotation, GradeInfo, CriteriaScores, AnnotationType } from '../../app/admin/klausur-korrektur/types'
import { ANNOTATION_COLORS } from '../../app/admin/klausur-korrektur/types'
interface CriteriaTabProps {
gradeInfo: GradeInfo
criteriaScores: CriteriaScores
annotations: Annotation[]
onCriteriaChange: (criterion: string, value: number) => void
onSelectTool: (tool: AnnotationType) => void
}
export default function CriteriaTab({
gradeInfo, criteriaScores, annotations, onCriteriaChange, onSelectTool,
}: CriteriaTabProps) {
return (
<>
{Object.entries(gradeInfo.criteria || {}).map(([key, criterion]) => {
const score = criteriaScores[key] || 0
const linkedAnnotations = annotations.filter(
(a) => a.linked_criterion === key || a.type === key
)
const errorCount = linkedAnnotations.length
const severityCounts = {
minor: linkedAnnotations.filter((a) => a.severity === 'minor').length,
major: linkedAnnotations.filter((a) => a.severity === 'major').length,
critical: linkedAnnotations.filter((a) => a.severity === 'critical').length,
}
const criterionColor = ANNOTATION_COLORS[key as AnnotationType] || '#6b7280'
return (
<div key={key} className="bg-slate-50 rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: criterionColor }}
/>
<span className="font-medium text-slate-800">{criterion.name}</span>
<span className="text-xs text-slate-500">({criterion.weight}%)</span>
</div>
<div className="text-lg font-bold text-slate-800">{score}%</div>
</div>
{/* Annotation count for this criterion */}
{errorCount > 0 && (
<div className="flex items-center gap-2 mb-2 text-xs">
<span className="text-slate-500">{errorCount} Markierungen:</span>
{severityCounts.minor > 0 && (
<span className="px-1.5 py-0.5 bg-yellow-100 text-yellow-700 rounded">
{severityCounts.minor} leicht
</span>
)}
{severityCounts.major > 0 && (
<span className="px-1.5 py-0.5 bg-orange-100 text-orange-700 rounded">
{severityCounts.major} mittel
</span>
)}
{severityCounts.critical > 0 && (
<span className="px-1.5 py-0.5 bg-red-100 text-red-700 rounded">
{severityCounts.critical} schwer
</span>
)}
</div>
)}
{/* Slider */}
<input
type="range"
min="0"
max="100"
value={score}
onChange={(e) => onCriteriaChange(key, parseInt(e.target.value))}
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer"
style={{ accentColor: criterionColor }}
/>
{/* Quick buttons */}
<div className="flex gap-1 mt-2">
{[0, 25, 50, 75, 100].map((val) => (
<button
key={val}
onClick={() => onCriteriaChange(key, val)}
className={`flex-1 py-1 text-xs rounded transition-colors ${
score === val
? 'text-white'
: 'bg-slate-200 text-slate-600 hover:bg-slate-300'
}`}
style={score === val ? { backgroundColor: criterionColor } : undefined}
>
{val}%
</button>
))}
</div>
{/* Quick add annotation button for RS/Grammatik */}
{(key === 'rechtschreibung' || key === 'grammatik') && (
<button
onClick={() => onSelectTool(key as AnnotationType)}
className="mt-2 w-full py-1 text-xs border rounded hover:bg-slate-100 flex items-center justify-center gap-1"
style={{ borderColor: criterionColor, color: criterionColor }}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{key === 'rechtschreibung' ? 'RS-Fehler' : 'Grammatik-Fehler'} markieren
</button>
)}
</div>
)
})}
</>
)
}