beginTransaction(); try { // Hauptdatensatz $stmt = $pdo->prepare(" INSERT INTO assessments_v2 (student_id, class_id, module_slug, module_version, spec_version, completed, abandoned, score, score_max, duration_s, difficulty, mode, module_specific_data, created_at) VALUES (:sid, :cid, :slug, :mv, :sv, :comp, :aban, :score, :scoremax, :dur, :diff, :mode, :msd, :ts) "); $stmt->execute([ ':sid' => $studentId, ':cid' => $classId, ':slug' => $body['moduleSlug'], ':mv' => $body['moduleVersion'], ':sv' => $body['specVersion'], ':comp' => (int) (bool) $body['completed'], ':aban' => (int) (bool) ($body['abandoned'] ?? false), ':score' => (int) $body['score'], ':scoremax' => (int) $body['scoreMax'], ':dur' => (int) $body['durationS'], ':diff' => $body['difficulty'], ':mode' => $body['mode'], ':msd' => isset($body['moduleSpecificData']) ? json_encode($body['moduleSpecificData'], JSON_UNESCAPED_UNICODE) : null, ':ts' => date('Y-m-d H:i:s', strtotime($body['ts']) ?: time()), ]); $assessmentId = (int) $pdo->lastInsertId(); // Milestones if (!empty($body['milestonesReached']) && is_array($body['milestonesReached'])) { $stmtMs = $pdo->prepare(" INSERT IGNORE INTO assessment_milestones_v2 (assessment_id, milestone_id) VALUES (:aid, :mid) "); foreach ($body['milestonesReached'] as $mid) { $stmtMs->execute([':aid' => $assessmentId, ':mid' => (string) $mid]); } } // Lehrplan-Coverage if (!empty($body['lehrplanCoverage']) && is_array($body['lehrplanCoverage'])) { $stmtLp = $pdo->prepare(" INSERT INTO assessment_lehrplan_v2 (assessment_id, anker_code, coverage) VALUES (:aid, :code, :cov) ON DUPLICATE KEY UPDATE coverage = VALUES(coverage) "); $stmtUpdLp = $pdo->prepare(" INSERT INTO lehrplan_progress_v2 (student_id, anker_code, best_coverage, reached_via_module) VALUES (:sid, :code, :cov, :slug) ON DUPLICATE KEY UPDATE best_coverage = CASE WHEN VALUES(best_coverage) = 'full' THEN 'full' WHEN best_coverage = 'full' THEN best_coverage WHEN VALUES(best_coverage) = 'partial' THEN 'partial' ELSE best_coverage END, reached_via_module = VALUES(reached_via_module), reached_at = CURRENT_TIMESTAMP "); foreach ($body['lehrplanCoverage'] as $code => $cov) { v2_validate_enum((string) $cov, ['full', 'partial', 'none'], "lehrplanCoverage.$code"); $stmtLp->execute([':aid' => $assessmentId, ':code' => $code, ':cov' => $cov]); if ($cov !== 'none') { $stmtUpdLp->execute([ ':sid' => $studentId, ':code' => $code, ':cov' => $cov, ':slug' => $body['moduleSlug'], ]); } } } // Freitext if (!empty($body['freeTextAnswers']) && is_array($body['freeTextAnswers'])) { $stmtFt = $pdo->prepare(" INSERT INTO assessment_freetext_v2 (assessment_id, prompt_id, prompt_text, answer_text) VALUES (:aid, :pid, :ptx, :atx) "); foreach ($body['freeTextAnswers'] as $ft) { if (!is_array($ft) || empty($ft['promptId'])) continue; $stmtFt->execute([ ':aid' => $assessmentId, ':pid' => (string) $ft['promptId'], ':ptx' => (string) ($ft['promptText'] ?? ''), ':atx' => (string) ($ft['answerText'] ?? ''), ]); } } // Artefakte verknüpfen (URLs aus separatem Upload, IDs aus body) if (!empty($body['artifacts']) && is_array($body['artifacts'])) { $stmtArt = $pdo->prepare(" INSERT IGNORE INTO assessment_artifacts_v2 (assessment_id, artifact_id) VALUES (:aid, :artid) "); foreach ($body['artifacts'] as $art) { if (is_array($art) && !empty($art['id'])) { $stmtArt->execute([':aid' => $assessmentId, ':artid' => (int) $art['id']]); } } } // Schüler-Modul-Fortschritt aktualisieren $stmt = $pdo->prepare(" INSERT INTO student_progress_v2 (student_id, module_slug, total_attempts, best_score, best_score_max, last_completed_at, total_duration_s) VALUES (:sid, :slug, 1, :score, :scoremax, CASE WHEN :comp THEN CURRENT_TIMESTAMP ELSE NULL END, :dur) ON DUPLICATE KEY UPDATE total_attempts = total_attempts + 1, best_score = GREATEST(best_score, VALUES(best_score)), best_score_max = GREATEST(best_score_max, VALUES(best_score_max)), last_completed_at = CASE WHEN :comp2 THEN CURRENT_TIMESTAMP ELSE last_completed_at END, total_duration_s = total_duration_s + VALUES(total_duration_s) "); $stmt->execute([ ':sid' => $studentId, ':slug' => $body['moduleSlug'], ':score' => (int) $body['score'], ':scoremax' => (int) $body['scoreMax'], ':comp' => (int) (bool) $body['completed'], ':comp2' => (int) (bool) $body['completed'], ':dur' => (int) $body['durationS'], ]); $pdo->commit(); v2_response_created([ 'ok' => true, 'assessmentId' => $assessmentId, 'savedAt' => date('c'), ]); } catch (Throwable $e) { $pdo->rollBack(); throw $e; }