Files
breakpilot-lehrer/website/components/klausur-korrektur/CorrectionPanel.tsx
Benjamin Admin 6811264756 [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>
2026-04-24 23:17:30 +02:00

228 lines
8.9 KiB
TypeScript

'use client'
/**
* Right panel (1/3 width) for the Korrektur-Workspace.
* Contains tabs: Kriterien, Annotationen, Gutachten, EH-Vorschlaege.
*/
import type {
Annotation, CriteriaScores, GradeInfo, AnnotationType,
} from '../../app/admin/klausur-korrektur/types'
import { ANNOTATION_COLORS } from '../../app/admin/klausur-korrektur/types'
import type { ExaminerWorkflow, ActiveTab } from './workspace-types'
import { GRADE_LABELS } from './workspace-types'
import CriteriaTab from './CriteriaTab'
import WorkflowActions from './WorkflowActions'
interface CorrectionPanelProps {
activeTab: ActiveTab
onTabChange: (tab: ActiveTab) => void
annotations: Annotation[]
gradeInfo: GradeInfo | null
criteriaScores: CriteriaScores
gutachten: string
totals: { gradePoints: number; weighted: number }
workflow: ExaminerWorkflow | null
saving: boolean
generatingGutachten: boolean
exporting: boolean
submittingWorkflow: boolean
selectedAnnotation: Annotation | null
studentId: string
klausurId: string
klausurEhId?: string
onCriteriaChange: (criterion: string, value: number) => void
onGutachtenChange: (text: string) => void
onSaveGutachten: () => void
onGenerateGutachten: () => void
onExportGutachtenPDF: () => void
onSelectAnnotation: (ann: Annotation | null) => void
onUpdateAnnotation: (id: string, updates: Partial<Annotation>) => void
onDeleteAnnotation: (id: string) => void
onSelectTool: (tool: AnnotationType) => void
onSetActiveTab: (tab: ActiveTab) => void
onSubmitErstkorrektur: () => void
onStartZweitkorrektur: (id: string) => void
onSubmitZweitkorrektur: () => void
onShowEinigungModal: () => void
// Render props for route-specific components
AnnotationPanelComponent: React.ComponentType<{
annotations: Annotation[]
selectedAnnotation: Annotation | null
onSelectAnnotation: (ann: Annotation | null) => void
onUpdateAnnotation: (id: string, updates: Partial<Annotation>) => void
onDeleteAnnotation: (id: string) => void
}>
EHSuggestionPanelComponent: React.ComponentType<{
studentId: string
klausurId: string
hasEH: boolean
apiBase: string
onInsertSuggestion: (text: string, criterion: string) => void
}>
}
export default function CorrectionPanel(props: CorrectionPanelProps) {
const {
activeTab, onTabChange, annotations, gradeInfo, criteriaScores, gutachten,
totals, workflow, saving, generatingGutachten, exporting, submittingWorkflow,
selectedAnnotation, studentId, klausurId, klausurEhId,
onCriteriaChange, onGutachtenChange, onSaveGutachten, onGenerateGutachten,
onExportGutachtenPDF, onSelectAnnotation, onUpdateAnnotation, onDeleteAnnotation,
onSelectTool, onSetActiveTab, onSubmitErstkorrektur, onStartZweitkorrektur,
onSubmitZweitkorrektur, onShowEinigungModal,
AnnotationPanelComponent, EHSuggestionPanelComponent,
} = props
const apiBase = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
return (
<div className="w-1/3 bg-white rounded-lg border border-slate-200 overflow-hidden flex flex-col">
{/* Tabs */}
<div className="border-b border-slate-200">
<nav className="flex">
{([
{ id: 'kriterien' as const, label: 'Kriterien' },
{ id: 'annotationen' as const, label: `Notizen (${annotations.length})` },
{ id: 'gutachten' as const, label: 'Gutachten' },
{ id: 'eh-vorschlaege' as const, label: 'EH' },
]).map(tab => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`flex-1 px-2 py-3 text-xs font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-primary-500 text-primary-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab content */}
<div className="flex-1 overflow-auto p-4">
{/* Kriterien Tab */}
{activeTab === 'kriterien' && gradeInfo && (
<div className="space-y-4">
<CriteriaTab
gradeInfo={gradeInfo}
criteriaScores={criteriaScores}
annotations={annotations}
onCriteriaChange={onCriteriaChange}
onSelectTool={onSelectTool}
/>
{/* Total and workflow actions */}
<div className="border-t border-slate-200 pt-4 mt-4">
<div className="flex justify-between items-center mb-4">
<span className="font-semibold text-slate-800">Gesamtergebnis</span>
<div className="text-right">
<div className="text-2xl font-bold text-primary-600">
{totals.gradePoints} Punkte
</div>
<div className="text-sm text-slate-500">
({totals.weighted}%) - Note {GRADE_LABELS[totals.gradePoints]}
</div>
</div>
</div>
<WorkflowActions
workflow={workflow}
gutachten={gutachten}
generatingGutachten={generatingGutachten}
submittingWorkflow={submittingWorkflow}
totals={totals}
onGenerateGutachten={onGenerateGutachten}
onSubmitErstkorrektur={onSubmitErstkorrektur}
onStartZweitkorrektur={onStartZweitkorrektur}
onSubmitZweitkorrektur={onSubmitZweitkorrektur}
onShowEinigungModal={onShowEinigungModal}
/>
</div>
</div>
)}
{/* Annotationen Tab */}
{activeTab === 'annotationen' && (
<div className="h-full -m-4">
<AnnotationPanelComponent
annotations={annotations}
selectedAnnotation={selectedAnnotation}
onSelectAnnotation={onSelectAnnotation}
onUpdateAnnotation={onUpdateAnnotation}
onDeleteAnnotation={onDeleteAnnotation}
/>
</div>
)}
{/* Gutachten Tab */}
{activeTab === 'gutachten' && (
<div className="h-full flex flex-col">
<textarea
value={gutachten}
onChange={(e) => onGutachtenChange(e.target.value)}
placeholder="Gutachten hier eingeben oder generieren lassen..."
className="flex-1 w-full p-3 border border-slate-300 rounded-lg resize-none focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
/>
<div className="flex gap-2 mt-4">
<button
onClick={onGenerateGutachten}
disabled={generatingGutachten}
className="flex-1 py-2 border border-primary-600 text-primary-600 rounded-lg hover:bg-primary-50 disabled:opacity-50"
>
{generatingGutachten ? 'Generiere...' : 'Neu generieren'}
</button>
<button
onClick={onSaveGutachten}
disabled={saving}
className="flex-1 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</div>
{/* PDF Export */}
{gutachten && (
<div className="flex gap-2 mt-2 pt-2 border-t border-slate-200">
<button
onClick={onExportGutachtenPDF}
disabled={exporting}
className="flex-1 py-2 border border-slate-300 text-slate-600 rounded-lg hover:bg-slate-50 disabled:opacity-50 flex items-center justify-center gap-2 text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{exporting ? 'Exportiere...' : 'Als PDF exportieren'}
</button>
</div>
)}
</div>
)}
{/* EH-Vorschlaege Tab */}
{activeTab === 'eh-vorschlaege' && (
<div className="h-full -m-4">
<EHSuggestionPanelComponent
studentId={studentId}
klausurId={klausurId}
hasEH={!!klausurEhId || true}
apiBase={apiBase}
onInsertSuggestion={(text, criterion) => {
onGutachtenChange(
gutachten
? `${gutachten}\n\n[${criterion.toUpperCase()}]: ${text}`
: `[${criterion.toUpperCase()}]: ${text}`
)
onSetActiveTab('gutachten')
}}
/>
</div>
)}
</div>
</div>
)
}