All 5 files reduced below 500 LOC (hard cap) by extracting sub-components: - training/page.tsx: 780→278 LOC — imports existing _components/, adds BlocksSection - control-provenance/page.tsx: 739→145 LOC — extracts provenance-data.ts, ProvenanceHelpers, LicenseMatrix, SourceRegistry - iace/[projectId]/verification/page.tsx: 673→164 LOC — extracts VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable - training/learner/page.tsx: 560→216 LOC — extracts AssignmentsList, ContentView, QuizView, CertificatesView - ControlDetail.tsx: 878→311 LOC — adds ControlSourceCitation, ControlTraceability, ControlRegulatorySection, ControlSimilarControls, ControlReviewActions siblings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
'use client'
|
|
|
|
import type { TrainingAssignment, QuizSubmitResponse } from '@/lib/sdk/training/types'
|
|
|
|
interface QuizQuestion {
|
|
id: string
|
|
question: string
|
|
options: string[]
|
|
difficulty: string
|
|
}
|
|
|
|
function formatTimer(seconds: number): string {
|
|
const m = Math.floor(seconds / 60)
|
|
const s = seconds % 60
|
|
return `${m}:${s.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
interface QuizViewProps {
|
|
questions: QuizQuestion[]
|
|
answers: Record<string, number>
|
|
quizResult: QuizSubmitResponse | null
|
|
quizSubmitting: boolean
|
|
quizTimer: number
|
|
selectedAssignment: TrainingAssignment | null
|
|
certGenerating: boolean
|
|
onAnswerChange: (questionId: string, optionIndex: number) => void
|
|
onSubmitQuiz: () => void
|
|
onRetryQuiz: () => void
|
|
onGenerateCertificate: (assignmentId: string) => void
|
|
}
|
|
|
|
export function QuizView({
|
|
questions, answers, quizResult, quizSubmitting, quizTimer,
|
|
selectedAssignment, certGenerating, onAnswerChange, onSubmitQuiz, onRetryQuiz, onGenerateCertificate,
|
|
}: QuizViewProps) {
|
|
if (questions.length === 0) {
|
|
return <div className="text-center py-12 text-gray-400">Starten Sie ein Quiz aus dem Schulungsinhalt-Tab</div>
|
|
}
|
|
|
|
if (quizResult) {
|
|
return (
|
|
<div className="max-w-lg mx-auto">
|
|
<div className={`text-center p-8 rounded-lg border-2 ${quizResult.passed ? 'border-green-300 bg-green-50' : 'border-red-300 bg-red-50'}`}>
|
|
<div className="text-4xl mb-3">{quizResult.passed ? '\u2705' : '\u274C'}</div>
|
|
<h2 className="text-2xl font-bold mb-2">{quizResult.passed ? 'Bestanden!' : 'Nicht bestanden'}</h2>
|
|
<p className="text-lg text-gray-700">{quizResult.correct_count} von {quizResult.total_count} richtig ({Math.round(quizResult.score)}%)</p>
|
|
<p className="text-sm text-gray-500 mt-1">Bestehensgrenze: {quizResult.threshold}% | Zeit: {formatTimer(quizTimer)}</p>
|
|
{quizResult.passed && selectedAssignment && !selectedAssignment.certificate_id && (
|
|
<button onClick={() => onGenerateCertificate(selectedAssignment.id)} disabled={certGenerating}
|
|
className="mt-4 px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50">
|
|
{certGenerating ? 'Erstelle Zertifikat...' : 'Zertifikat generieren & herunterladen'}
|
|
</button>
|
|
)}
|
|
{!quizResult.passed && (
|
|
<button onClick={onRetryQuiz} className="mt-4 px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
|
|
Quiz erneut versuchen
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Quiz — {selectedAssignment?.module_title}</h2>
|
|
<span className="text-sm text-gray-500 font-mono bg-gray-100 px-3 py-1 rounded">{formatTimer(quizTimer)}</span>
|
|
</div>
|
|
<div className="space-y-6">
|
|
{questions.map((q, idx) => (
|
|
<div key={q.id} className="bg-white border border-gray-200 rounded-lg p-5">
|
|
<p className="font-medium text-gray-900 mb-3">
|
|
<span className="text-indigo-600 mr-2">Frage {idx + 1}.</span>{q.question}
|
|
</p>
|
|
<div className="space-y-2">
|
|
{q.options.map((opt, oi) => (
|
|
<label key={oi} className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
|
|
answers[q.id] === oi ? 'border-indigo-500 bg-indigo-50' : 'border-gray-200 hover:bg-gray-50'
|
|
}`}>
|
|
<input type="radio" name={q.id} checked={answers[q.id] === oi}
|
|
onChange={() => onAnswerChange(q.id, oi)} className="text-indigo-600" />
|
|
<span className="text-sm text-gray-700">{opt}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-6 flex justify-end">
|
|
<button onClick={onSubmitQuiz} disabled={quizSubmitting || Object.keys(answers).length < questions.length}
|
|
className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50">
|
|
{quizSubmitting ? 'Wird ausgewertet...' : `Quiz abgeben (${Object.keys(answers).length}/${questions.length})`}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|