[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:
227
website/components/klausur-korrektur/CorrectionPanel.tsx
Normal file
227
website/components/klausur-korrektur/CorrectionPanel.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user