[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:
120
website/components/klausur-korrektur/CriteriaTab.tsx
Normal file
120
website/components/klausur-korrektur/CriteriaTab.tsx
Normal 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>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user