[split-required] Split 500-1000 LOC files across all services
backend-lehrer (5 files): - alerts_agent/db/repository.py (992 → 5), abitur_docs_api.py (956 → 3) - teacher_dashboard_api.py (951 → 3), services/pdf_service.py (916 → 3) - mail/mail_db.py (987 → 6) klausur-service (5 files): - legal_templates_ingestion.py (942 → 3), ocr_pipeline_postprocess.py (929 → 4) - ocr_pipeline_words.py (876 → 3), ocr_pipeline_ocr_merge.py (616 → 2) - KorrekturPage.tsx (956 → 6) website (5 pages): - mail (985 → 9), edu-search (958 → 8), mac-mini (950 → 7) - ocr-labeling (946 → 7), audit-workspace (871 → 4) studio-v2 (5 files + 1 deleted): - page.tsx (946 → 5), MessagesContext.tsx (925 → 4) - korrektur (914 → 6), worksheet-cleanup (899 → 6) - useVocabWorksheet.ts (888 → 3) - Deleted dead page-original.tsx (934 LOC) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* KorrekturDocumentViewer — center panel document display.
|
||||
*
|
||||
* Extracted from KorrekturPage.tsx.
|
||||
*/
|
||||
|
||||
import { StudentKlausur } from '../services/api'
|
||||
|
||||
interface KorrekturDocumentViewerProps {
|
||||
currentStudent: StudentKlausur | null
|
||||
}
|
||||
|
||||
export default function KorrekturDocumentViewer({ currentStudent }: KorrekturDocumentViewerProps) {
|
||||
return (
|
||||
<div className="korrektur-main">
|
||||
<div className="viewer-container">
|
||||
<div className="viewer-toolbar">
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>
|
||||
{currentStudent ? currentStudent.student_name : 'Dokument-Ansicht'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{currentStudent && (
|
||||
<>
|
||||
<button className="btn btn-ghost" style={{ padding: '6px 12px' }}>
|
||||
OCR-Text
|
||||
</button>
|
||||
<button className="btn btn-ghost" style={{ padding: '6px 12px' }}>
|
||||
Original
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="viewer-content">
|
||||
{!currentStudent ? (
|
||||
<div className="document-placeholder">
|
||||
<div className="document-placeholder-icon">{'\uD83D\uDCC4'}</div>
|
||||
<div style={{ fontSize: 16, marginBottom: 8 }}>Keine Arbeit ausgewaehlt</div>
|
||||
<div style={{ fontSize: 14 }}>
|
||||
Waehlen Sie eine Schuelerarbeit aus der Liste oder laden Sie eine neue hoch
|
||||
</div>
|
||||
</div>
|
||||
) : currentStudent.file_path ? (
|
||||
<div className="document-viewer">
|
||||
<div className="document-info-bar">
|
||||
<span className="file-name">{'\uD83D\uDCC4'} {currentStudent.student_name}</span>
|
||||
<span className="file-status">{'\u2713'} Hochgeladen</span>
|
||||
</div>
|
||||
<div className="document-frame">
|
||||
{currentStudent.file_path.endsWith('.pdf') ? (
|
||||
<iframe
|
||||
src={`/api/v1/students/${currentStudent.id}/file`}
|
||||
title="Schuelerarbeit"
|
||||
style={{ width: '100%', height: '100%', minHeight: '600px' }}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={`/api/v1/students/${currentStudent.id}/file`}
|
||||
alt={`Arbeit von ${currentStudent.student_name}`}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="document-placeholder">
|
||||
<div className="document-placeholder-icon">{'\uD83D\uDCC4'}</div>
|
||||
<div style={{ fontSize: 16, marginBottom: 8 }}>Keine Datei vorhanden</div>
|
||||
<div style={{ fontSize: 14 }}>
|
||||
Laden Sie eine Schuelerarbeit hoch, um mit der Korrektur zu beginnen.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
197
klausur-service/frontend/src/components/KorrekturModals.tsx
Normal file
197
klausur-service/frontend/src/components/KorrekturModals.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* KorrekturModals — upload modal and EH prompt modal.
|
||||
*
|
||||
* Extracted from KorrekturPage.tsx.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Upload Modal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface UploadModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
studentName: string
|
||||
onStudentNameChange: (name: string) => void
|
||||
classStudents: Array<{ id: string; name: string }>
|
||||
onUpload: () => void
|
||||
uploading: boolean
|
||||
selectedFile: File | null
|
||||
onFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export function UploadModal({
|
||||
open,
|
||||
onClose,
|
||||
studentName,
|
||||
onStudentNameChange,
|
||||
classStudents,
|
||||
onUpload,
|
||||
uploading,
|
||||
selectedFile,
|
||||
onFileSelect,
|
||||
}: UploadModalProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [useStudentDropdown, setUseStudentDropdown] = useState(true)
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title">Schuelerarbeit hochladen</div>
|
||||
<button className="modal-close" onClick={onClose}>{'\u00D7'}</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Schueler zuweisen</label>
|
||||
|
||||
{classStudents.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useStudentDropdown}
|
||||
onChange={(e) => setUseStudentDropdown(e.target.checked)}
|
||||
/>
|
||||
Aus Klassenliste waehlen
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{useStudentDropdown && classStudents.length > 0 ? (
|
||||
<select
|
||||
className="input"
|
||||
value={studentName}
|
||||
onChange={e => onStudentNameChange(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<option value="">-- Schueler waehlen --</option>
|
||||
{classStudents.map(s => (
|
||||
<option key={s.id} value={s.name}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="z.B. Max Mustermann"
|
||||
value={studentName}
|
||||
onChange={e => onStudentNameChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{classStudents.length === 0 && (
|
||||
<p style={{ fontSize: 12, color: 'var(--bp-text-muted)', marginTop: 8 }}>
|
||||
Keine Klassenliste verfuegbar. Bitte Namen manuell eingeben.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Datei (PDF oder Bild)</label>
|
||||
<div
|
||||
className={`upload-area ${selectedFile ? 'selected' : ''}`}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".pdf,.png,.jpg,.jpeg"
|
||||
style={{ display: 'none' }}
|
||||
onChange={onFileSelect}
|
||||
/>
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<div className="upload-icon">{'\uD83D\uDCC4'}</div>
|
||||
<div className="upload-text">{selectedFile.name}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="upload-icon">{'\uD83D\uDCC1'}</div>
|
||||
<div className="upload-text">
|
||||
Klicken Sie hier oder ziehen Sie eine Datei hinein
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={onClose}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!studentName || !selectedFile || uploading}
|
||||
onClick={onUpload}
|
||||
>
|
||||
{uploading ? 'Wird hochgeladen...' : 'Hochladen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EH Prompt Modal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface EHPromptModalProps {
|
||||
open: boolean
|
||||
onUpload: () => void
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function EHPromptModal({ open, onUpload, onDismiss }: EHPromptModalProps) {
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onDismiss}>
|
||||
<div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 500 }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title">{'\uD83D\uDCCB'} Erwartungshorizont hochladen?</div>
|
||||
<button className="modal-close" onClick={onDismiss}>{'\u00D7'}</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p style={{ marginBottom: 16, lineHeight: 1.6 }}>
|
||||
Sie haben die erste Schuelerarbeit hochgeladen. Moechten Sie jetzt einen
|
||||
<strong> Erwartungshorizont</strong> hinzufuegen?
|
||||
</p>
|
||||
|
||||
<div style={{
|
||||
background: 'var(--bp-bg-light)',
|
||||
border: '1px solid var(--bp-border)',
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8 }}>{'\u2713'} Vorteile:</div>
|
||||
<ul style={{ margin: 0, paddingLeft: 20, color: 'var(--bp-text-muted)', fontSize: 14, lineHeight: 1.8 }}>
|
||||
<li>KI-gestuetzte Korrekturvorschlaege basierend auf Ihrem EH</li>
|
||||
<li>Bessere und konsistentere Bewertungen</li>
|
||||
<li>Automatisch fuer alle Korrektoren verfuegbar</li>
|
||||
<li>Ende-zu-Ende verschluesselt - nur Sie haben den Schluessel</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: 13, color: 'var(--bp-text-muted)' }}>
|
||||
Sie koennen den Erwartungshorizont auch spaeter hochladen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={onDismiss}>
|
||||
Spaeter
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={onUpload}>
|
||||
Jetzt hochladen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
klausur-service/frontend/src/components/KorrekturSidebar.tsx
Normal file
101
klausur-service/frontend/src/components/KorrekturSidebar.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* KorrekturSidebar — collapsible left sidebar with klausur info and student list.
|
||||
*
|
||||
* Extracted from KorrekturPage.tsx.
|
||||
*/
|
||||
|
||||
import { StudentKlausur } from '../services/api'
|
||||
|
||||
interface KorrekturSidebarProps {
|
||||
collapsed: boolean
|
||||
onToggle: () => void
|
||||
klausurTitle: string
|
||||
klausurModus: string
|
||||
students: StudentKlausur[]
|
||||
currentStudentId?: string
|
||||
onSelectStudent: (id: string) => void
|
||||
onDeleteStudent: (id: string, e: React.MouseEvent) => void
|
||||
onUploadClick: () => void
|
||||
}
|
||||
|
||||
export default function KorrekturSidebar({
|
||||
collapsed,
|
||||
onToggle,
|
||||
klausurTitle,
|
||||
klausurModus,
|
||||
students,
|
||||
currentStudentId,
|
||||
onSelectStudent,
|
||||
onDeleteStudent,
|
||||
onUploadClick,
|
||||
}: KorrekturSidebarProps) {
|
||||
return (
|
||||
<div className={`korrektur-sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||
<button
|
||||
className="sidebar-toggle"
|
||||
onClick={onToggle}
|
||||
title={collapsed ? 'Sidebar einblenden' : 'Sidebar ausblenden'}
|
||||
>
|
||||
{collapsed ? '\u2192' : '\u2190'}
|
||||
</button>
|
||||
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section-title">Klausur</div>
|
||||
<div className="klausur-item active">
|
||||
<div className="klausur-icon">{'\uD83D\uDCCB'}</div>
|
||||
<div className="klausur-info">
|
||||
<div className="klausur-name">{klausurTitle}</div>
|
||||
<div className="klausur-meta">
|
||||
{klausurModus === 'landes_abitur' ? 'Abitur' : 'Vorabitur'} {'\u2022'} {students.length} Schueler
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-section" style={{ flex: 1 }}>
|
||||
<div className="sidebar-section-title">Schuelerarbeiten</div>
|
||||
|
||||
{students.length === 0 ? (
|
||||
<div style={{ color: 'var(--bp-text-muted)', fontSize: 13, padding: '8px 0' }}>
|
||||
Noch keine Arbeiten hochgeladen
|
||||
</div>
|
||||
) : (
|
||||
students.map((student) => (
|
||||
<div
|
||||
key={student.id}
|
||||
className={`klausur-item ${currentStudentId === student.id ? 'active' : ''}`}
|
||||
onClick={() => onSelectStudent(student.id)}
|
||||
>
|
||||
<div className="klausur-icon">{'\uD83D\uDCC4'}</div>
|
||||
<div className="klausur-info">
|
||||
<div className="klausur-name">{student.student_name}</div>
|
||||
<div className="klausur-meta">
|
||||
{student.status === 'completed' ? `${student.grade_points} Punkte` : student.status}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="delete-btn"
|
||||
onClick={(e) => onDeleteStudent(student.id, e)}
|
||||
title="Loeschen"
|
||||
>
|
||||
{'\uD83D\uDDD1\uFE0F'}
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ width: '100%', marginTop: 16 }}
|
||||
onClick={onUploadClick}
|
||||
>
|
||||
+ Arbeit hochladen
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
285
klausur-service/frontend/src/components/KorrekturWizardSteps.tsx
Normal file
285
klausur-service/frontend/src/components/KorrekturWizardSteps.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* KorrekturWizardSteps — right panel wizard content for korrektur, bewertung, gutachten.
|
||||
*
|
||||
* Extracted from KorrekturPage.tsx.
|
||||
*/
|
||||
|
||||
import { StudentKlausur } from '../services/api'
|
||||
import { CRITERIA, GRADE_LABELS, WizardStep } from '../pages/KorrekturConstants'
|
||||
|
||||
interface WizardStepIndicatorProps {
|
||||
current: WizardStep
|
||||
}
|
||||
|
||||
function WizardStepIndicator({ current }: WizardStepIndicatorProps) {
|
||||
const steps: { key: WizardStep; label: string; number: string }[] = [
|
||||
{ key: 'korrektur', label: 'Korrektur', number: '1' },
|
||||
{ key: 'bewertung', label: 'Bewertung', number: '2' },
|
||||
{ key: 'gutachten', label: 'Gutachten', number: '3' },
|
||||
]
|
||||
|
||||
const currentIdx = steps.findIndex(s => s.key === current)
|
||||
|
||||
return (
|
||||
<div className="wizard-steps">
|
||||
{steps.map((step, idx) => {
|
||||
const isCompleted = idx < currentIdx
|
||||
const isActive = idx === currentIdx
|
||||
return (
|
||||
<div key={step.key} className={`wizard-step ${isCompleted ? 'completed' : ''} ${isActive ? 'active' : ''}`}>
|
||||
<span className="wizard-step-number">{isCompleted ? '\u2713' : step.number}</span>
|
||||
<span className="wizard-step-label">{step.label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface KorrekturStepProps {
|
||||
korrekturNotes: string
|
||||
onNotesChange: (notes: string) => void
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
export function KorrekturStep({ korrekturNotes, onNotesChange, onComplete }: KorrekturStepProps) {
|
||||
return (
|
||||
<>
|
||||
<WizardStepIndicator current="korrektur" />
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">{'\u270F\uFE0F'} Korrektur durchfuehren</div>
|
||||
<p style={{ color: 'var(--bp-text-muted)', fontSize: 13, marginBottom: 16 }}>
|
||||
Lesen Sie die Arbeit sorgfaeltig und machen Sie Anmerkungen direkt im Dokument.
|
||||
Notieren Sie hier Ihre wichtigsten Beobachtungen.
|
||||
</p>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Korrektur-Notizen</label>
|
||||
<textarea
|
||||
className="textarea"
|
||||
placeholder="Ihre Notizen waehrend der Korrektur..."
|
||||
style={{ minHeight: 200 }}
|
||||
value={korrekturNotes}
|
||||
onChange={(e) => onNotesChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
onClick={onComplete}
|
||||
>
|
||||
Weiter zur Bewertung {'\u2192'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface BewertungStepProps {
|
||||
gradePoints: number
|
||||
totalPercentage: number
|
||||
localScores: Record<string, number>
|
||||
savingCriteria: boolean
|
||||
allCriteriaFilled: boolean
|
||||
onCriteriaChange: (criterion: string, value: number) => void
|
||||
onBack: () => void
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
export function BewertungStep({
|
||||
gradePoints,
|
||||
totalPercentage,
|
||||
localScores,
|
||||
savingCriteria,
|
||||
allCriteriaFilled,
|
||||
onCriteriaChange,
|
||||
onBack,
|
||||
onComplete,
|
||||
}: BewertungStepProps) {
|
||||
return (
|
||||
<>
|
||||
<WizardStepIndicator current="bewertung" />
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">{'\uD83D\uDCCA'} Gesamtnote</div>
|
||||
<div className="grade-display">
|
||||
<div className="grade-points">{gradePoints}</div>
|
||||
<div className="grade-label">
|
||||
{GRADE_LABELS[gradePoints]} ({totalPercentage}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">
|
||||
{'\u270F\uFE0F'} Bewertungskriterien
|
||||
{savingCriteria && <span style={{ fontSize: 11, color: 'var(--bp-text-muted)' }}> (Speichert...)</span>}
|
||||
</div>
|
||||
|
||||
{CRITERIA.map(c => (
|
||||
<div key={c.key} className="criterion-item">
|
||||
<div className="criterion-header">
|
||||
<span className="criterion-label">{c.label} ({Math.round(c.weight * 100)}%)</span>
|
||||
<span className="criterion-score">{localScores[c.key] || 0}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
className="criterion-slider"
|
||||
min="0"
|
||||
max="100"
|
||||
value={localScores[c.key] || 0}
|
||||
onChange={(e) => onCriteriaChange(c.key, Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ flex: 1 }}
|
||||
onClick={onBack}
|
||||
>
|
||||
{'\u2190'} Zurueck
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ flex: 1 }}
|
||||
onClick={onComplete}
|
||||
disabled={!allCriteriaFilled}
|
||||
>
|
||||
Weiter {'\u2192'}
|
||||
</button>
|
||||
</div>
|
||||
{!allCriteriaFilled && (
|
||||
<p style={{ color: 'var(--bp-warning)', fontSize: 12, marginTop: 8, textAlign: 'center' }}>
|
||||
Bitte alle Kriterien bewerten
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface GutachtenStepProps {
|
||||
gradePoints: number
|
||||
currentStudent: StudentKlausur
|
||||
localGutachten: { einleitung: string; hauptteil: string; fazit: string }
|
||||
generatingGutachten: boolean
|
||||
savingGutachten: boolean
|
||||
finalizingStudent: boolean
|
||||
onGutachtenChange: (field: 'einleitung' | 'hauptteil' | 'fazit', value: string) => void
|
||||
onGenerate: () => void
|
||||
onSave: () => void
|
||||
onFinalize: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function GutachtenStep({
|
||||
gradePoints,
|
||||
currentStudent,
|
||||
localGutachten,
|
||||
generatingGutachten,
|
||||
savingGutachten,
|
||||
finalizingStudent,
|
||||
onGutachtenChange,
|
||||
onGenerate,
|
||||
onSave,
|
||||
onFinalize,
|
||||
onBack,
|
||||
}: GutachtenStepProps) {
|
||||
return (
|
||||
<>
|
||||
<WizardStepIndicator current="gutachten" />
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">
|
||||
{'\uD83D\uDCCA'} Endergebnis: {gradePoints} Punkte ({GRADE_LABELS[gradePoints]})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">
|
||||
{'\uD83D\uDCDD'} Gutachten
|
||||
{savingGutachten && <span style={{ fontSize: 11, color: 'var(--bp-text-muted)' }}> (Speichert...)</span>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ width: '100%', marginBottom: 16 }}
|
||||
onClick={onGenerate}
|
||||
disabled={generatingGutachten}
|
||||
>
|
||||
{generatingGutachten ? '\u231B KI generiert...' : '\uD83E\uDD16 KI-Gutachten generieren'}
|
||||
</button>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Einleitung</label>
|
||||
<textarea
|
||||
className="textarea"
|
||||
placeholder="Allgemeine Einordnung der Arbeit..."
|
||||
value={localGutachten.einleitung}
|
||||
onChange={(e) => onGutachtenChange('einleitung', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Hauptteil</label>
|
||||
<textarea
|
||||
className="textarea"
|
||||
placeholder="Detaillierte Bewertung..."
|
||||
style={{ minHeight: 120 }}
|
||||
value={localGutachten.hauptteil}
|
||||
onChange={(e) => onGutachtenChange('hauptteil', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Fazit</label>
|
||||
<textarea
|
||||
className="textarea"
|
||||
placeholder="Zusammenfassung und Empfehlungen..."
|
||||
value={localGutachten.fazit}
|
||||
onChange={(e) => onGutachtenChange('fazit', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ width: '100%', marginBottom: 8 }}
|
||||
onClick={onSave}
|
||||
disabled={savingGutachten}
|
||||
>
|
||||
{savingGutachten ? '\uD83D\uDCBE Speichert...' : '\uD83D\uDCBE Gutachten speichern'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ flex: 1 }}
|
||||
onClick={onBack}
|
||||
>
|
||||
{'\u2190'} Zurueck
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
onClick={onFinalize}
|
||||
disabled={finalizingStudent || currentStudent.status === 'completed'}
|
||||
>
|
||||
{currentStudent.status === 'completed'
|
||||
? '\u2713 Abgeschlossen'
|
||||
: finalizingStudent
|
||||
? 'Wird abgeschlossen...'
|
||||
: '\u2713 Bewertung abschliessen'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
37
klausur-service/frontend/src/pages/KorrekturConstants.ts
Normal file
37
klausur-service/frontend/src/pages/KorrekturConstants.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* KorrekturPage Constants — grade tables, criteria definitions, types.
|
||||
*
|
||||
* Extracted from KorrekturPage.tsx.
|
||||
*/
|
||||
|
||||
// Grade calculation
|
||||
export const GRADE_THRESHOLDS: Record<number, number> = {
|
||||
15: 95, 14: 90, 13: 85, 12: 80, 11: 75, 10: 70,
|
||||
9: 65, 8: 60, 7: 55, 6: 50, 5: 45, 4: 40,
|
||||
3: 33, 2: 27, 1: 20, 0: 0
|
||||
}
|
||||
|
||||
export const GRADE_LABELS: Record<number, string> = {
|
||||
15: '1+', 14: '1', 13: '1-', 12: '2+', 11: '2', 10: '2-',
|
||||
9: '3+', 8: '3', 7: '3-', 6: '4+', 5: '4', 4: '4-',
|
||||
3: '5+', 2: '5', 1: '5-', 0: '6'
|
||||
}
|
||||
|
||||
export const CRITERIA = [
|
||||
{ key: 'inhalt', label: 'Inhaltliche Leistung', weight: 0.40 },
|
||||
{ key: 'struktur', label: 'Aufbau & Struktur', weight: 0.15 },
|
||||
{ key: 'stil', label: 'Ausdruck & Stil', weight: 0.15 },
|
||||
{ key: 'grammatik', label: 'Grammatik', weight: 0.15 },
|
||||
{ key: 'rechtschreibung', label: 'Rechtschreibung', weight: 0.15 }
|
||||
] as const
|
||||
|
||||
export type WizardStep = 'korrektur' | 'bewertung' | 'gutachten'
|
||||
|
||||
export function calculateGradePoints(percentage: number): number {
|
||||
for (const [points, threshold] of Object.entries(GRADE_THRESHOLDS).sort((a, b) => Number(b[0]) - Number(a[0]))) {
|
||||
if (percentage >= threshold) {
|
||||
return Number(points)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user