add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
+656
View File
@@ -0,0 +1,656 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\Configuration;
use App\Models\Quiz;
use App\Models\Student;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use App\Models\User;
use App\Services\SemesterScoreService;
use RuntimeException;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
class QuizController extends BaseApiController
{
protected Quiz $quiz;
protected Configuration $config;
protected Student $student;
protected StudentClass $studentClass;
protected TeacherClass $teacherClass;
protected User $user;
protected SemesterScoreService $semesterScoreService;
protected string $schoolYear;
protected string $semester;
public function __construct()
{
parent::__construct();
$this->quiz = model(Quiz::class);
$this->config = model(Configuration::class);
$this->student = model(Student::class);
$this->studentClass = model(StudentClass::class);
$this->teacherClass = model(TeacherClass::class);
$this->user = model(User::class);
$this->semesterScoreService = app(SemesterScoreService::class);
$this->schoolYear = (string) ($this->config->getConfig('school_year') ?? '');
$this->semester = (string) ($this->config->getConfig('semester') ?? '');
}
/**
* GET /api/v1/quizzes
* List quizzes with optional filters
*/
public function index()
{
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
$perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20)));
$studentId = $this->request->getGet('student_id');
$classSectionId = $this->request->getGet('class_section_id');
$query = $this->quiz->newQuery()
->where('school_year', $this->schoolYear)
->where('semester', $this->semester);
if ($studentId) {
$query->where('student_id', $studentId);
}
if ($classSectionId) {
$query->where('class_section_id', $classSectionId);
}
$result = $this->paginate($query, $page, $perPage);
return $this->success($result, 'Quizzes retrieved successfully');
}
/**
* GET /api/v1/quizzes/{id}
* Get a single quiz
*/
public function show($id = null)
{
$quiz = $this->quiz->find($id);
if (!$quiz) {
return $this->error('Quiz not found', Response::HTTP_NOT_FOUND);
}
return $this->success($quiz, 'Quiz retrieved successfully');
}
/**
* GET /api/v1/quizzes/student/{id}
* Get quizzes for a specific student
*/
public function getByStudent($id = null)
{
$quizzes = $this->quiz->newQuery()
->where('student_id', $id)
->where('school_year', $this->schoolYear)
->where('semester', $this->semester)
->orderBy('quiz_index', 'ASC')
->get()
->toArray();
return $this->success($quizzes, 'Quizzes retrieved successfully');
}
/**
* POST /api/v1/quizzes
* Create a new quiz entry
*/
public function store()
{
$user = $this->getCurrentUser();
if (!$user) {
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
}
$data = $this->payloadData();
if (empty($data)) {
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
}
$rules = [
'student_id' => 'required|integer',
'class_section_id' => 'required|integer',
'quiz_index' => 'required|integer',
];
$errors = $this->validateRequest($data, $rules);
if (!empty($errors)) {
return $this->respondValidationError($errors);
}
$quizData = [
'student_id' => $data['student_id'],
'class_section_id' => $data['class_section_id'],
'quiz_index' => $data['quiz_index'],
'score' => $data['score'] ?? null,
'comment' => $data['comment'] ?? null,
'school_year' => $this->schoolYear,
'semester' => $this->semester,
'updated_by' => $user->id,
];
try {
$quiz = $this->quiz->create($quizData);
return $this->success($quiz->toArray(), 'Quiz created successfully', Response::HTTP_CREATED);
} catch (\Throwable $e) {
log_message('error', 'Quiz creation error: ' . $e->getMessage());
return $this->respondError('Failed to create quiz', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* PATCH /api/v1/quizzes/{id}
* Update a quiz entry
*/
public function update($id = null)
{
$quiz = $this->quiz->find($id);
if (!$quiz) {
return $this->error('Quiz not found', Response::HTTP_NOT_FOUND);
}
$user = $this->getCurrentUser();
if (!$user) {
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
}
$data = $this->payloadData();
if (empty($data)) {
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
}
$allowedFields = ['score', 'comment', 'quiz_index'];
$updateData = [];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $data)) {
$updateData[$field] = $data[$field];
}
}
if (empty($updateData)) {
return $this->error('No valid fields to update', Response::HTTP_BAD_REQUEST);
}
$updateData['updated_by'] = $user->id;
$updateData['updated_at'] = utc_now();
try {
$this->quiz->update($id, $updateData);
$updatedQuiz = $this->quiz->find($id);
return $this->success($updatedQuiz, 'Quiz updated successfully');
} catch (\Throwable $e) {
log_message('error', 'Quiz update error: ' . $e->getMessage());
return $this->respondError('Failed to update quiz', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* GET /api/v1/quizzes/add-quiz
* Get quiz data for teacher's class section (equivalent to addQuiz view)
*/
public function addQuiz()
{
// Get class_section_id from POST -> GET -> session
$incoming = $this->request->getPost('class_section_id')
?? $this->request->getGet('class_section_id')
?? session()->get('class_section_id');
$classSectionId = (int) ($incoming ?: 0);
if ($classSectionId <= 0) {
return $this->respondError('Missing class section.', Response::HTTP_BAD_REQUEST);
}
session()->put('class_section_id', $classSectionId);
// Get distinct quiz_index values for this class/term
$quizHeaderRows = $this->quiz
->select('quiz_index')
->where('class_section_id', $classSectionId)
->where('school_year', $this->schoolYear)
->groupBy('quiz_index')
->orderBy('quiz_index', 'ASC')
->findAll();
$quizHeaders = array_map(static fn($r) => (int)$r['quiz_index'], $quizHeaderRows);
// Get roster (distinct students for this section/term)
$roster = DB::table('student_class as sc')
->select('s.id AS student_id', 's.firstname', 's.lastname', 's.school_id')
->distinct()
->join('students as s', 's.id', '=', 'sc.student_id')
->where('sc.class_section_id', $classSectionId)
->where('sc.school_year', $this->schoolYear)
->orderBy('s.lastname', 'ASC')
->orderBy('s.firstname', 'ASC')
->get()
->toArray();
// Get all existing quiz scores
$scoreRows = $this->quiz
->select('id, student_id, quiz_index, score')
->where('class_section_id', $classSectionId)
->where('school_year', $this->schoolYear)
->findAll();
$scoresMap = []; // [student_id][quiz_index] => score
$idMap = []; // [student_id][quiz_index] => id
foreach ($scoreRows as $r) {
$sid = (int)$r['student_id'];
$qi = (int)$r['quiz_index'];
$scoresMap[$sid][$qi] = $r['score'];
$idMap[$sid][$qi] = (int)$r['id'];
}
// Build students array with per-quiz scores
$students = [];
foreach ($roster as $st) {
$sid = (int)$st->student_id;
$quizScores = [];
foreach ($quizHeaders as $qi) {
$quizScores[$qi] = $scoresMap[$sid][$qi] ?? '';
}
$students[] = [
'student_id' => $sid,
'firstname' => $st->firstname,
'lastname' => $st->lastname,
'school_id' => $st->school_id ?? null,
'scores' => $quizScores,
'existingIds' => $idMap[$sid] ?? [],
];
}
return $this->success([
'students' => $students,
'quiz_headers' => $quizHeaders,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
'class_section_id' => $classSectionId
], 'Quiz data retrieved successfully');
}
/**
* POST /api/v1/quizzes/update-scores
* Update quiz scores for multiple students
*/
public function updateQuizScores()
{
$updatedBy = $this->getCurrentUserId();
if (!$updatedBy) {
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
}
$payload = $this->payloadData();
$scores = $payload['scores'] ?? null;
$classSectionId = (int) ($payload['class_section_id'] ?? session()->get('class_section_id') ?? 0);
if ($classSectionId <= 0) {
return $this->respondError('Class section ID is required', Response::HTTP_BAD_REQUEST);
}
if (!is_array($scores)) {
return $this->respondError('No quiz scores submitted.', Response::HTTP_BAD_REQUEST);
}
try {
foreach ($scores as $studentId => $quizData) {
if (!is_numeric($studentId) || $studentId <= 0) continue;
// Normalize: if single input submitted
if (!is_array($quizData)) {
$quizData = [1 => $quizData];
}
foreach ($quizData as $quizNumber => $score) {
if (!is_numeric($quizNumber) || !is_numeric($score)) continue;
$existing = $this->quiz->where([
'student_id' => $studentId,
'quiz_index' => $quizNumber,
'class_section_id' => $classSectionId,
'semester' => $this->semester
])->first();
if ($existing) {
$this->quiz->update($existing['id'], [
'score' => is_numeric($score) ? (float)$score : null,
'updated_at' => utc_now()
]);
} else {
$student = $this->student->find($studentId);
$this->quiz->insert([
'student_id' => $studentId,
'school_id' => $student['school_id'] ?? '',
'class_section_id' => $classSectionId,
'updated_by' => $updatedBy,
'quiz_index' => $quizNumber,
'score' => is_numeric($score) ? (float)$score : null,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
'created_at' => utc_now(),
'updated_at' => utc_now()
]);
}
}
}
$studentTeacherInfo = $this->student->getStudentInfoByClassSectionId($classSectionId);
// Call the updateScoresForStudents method
try {
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo);
} catch (RuntimeException $e) {
log_message('error', 'Failed to update semester scores: ' . $e->getMessage());
}
return $this->success(null, 'Quiz scores updated successfully.');
} catch (\Throwable $e) {
log_message('error', 'Update quiz scores error: ' . $e->getMessage());
return $this->respondError('Failed to update quiz scores: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* POST /api/v1/quizzes/add-column
* Add a new quiz column for all students in a class section
*/
public function addNextQuizColumn()
{
$updatedBy = $this->getCurrentUserId();
if (!$updatedBy) {
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
}
$payload = $this->payloadData();
$classSectionId = (int) ($payload['class_section_id'] ?? session()->get('class_section_id') ?? 0);
if ($classSectionId <= 0) {
return $this->respondError('Class section ID is required', Response::HTTP_BAD_REQUEST);
}
try {
$existingQuizNumbers = $this->quiz
->select('quiz_index')
->where('class_section_id', $classSectionId)
->where('updated_by', $updatedBy)
->where('semester', $this->semester)
->where('school_year', $this->schoolYear)
->groupBy('quiz_index')
->orderBy('quiz_index', 'DESC')
->findAll();
$maxQuizNumber = 0;
foreach ($existingQuizNumbers as $row) {
if (is_numeric($row['quiz_index'])) {
$maxQuizNumber = max($maxQuizNumber, (int)$row['quiz_index']);
}
}
$nextQuizNumber = $maxQuizNumber + 1;
if ($nextQuizNumber <= 0) {
return $this->respondError('Invalid quiz number', Response::HTTP_BAD_REQUEST);
}
$students = $this->studentClass
->where('class_section_id', $classSectionId)
->findAll();
if (empty($students)) {
return $this->respondError('No students found in this class section.', Response::HTTP_NOT_FOUND);
}
$firstInsertedId = null;
$insertedCount = 0;
foreach ($students as $i => $student) {
$exists = $this->quiz->where([
'student_id' => $student['student_id'],
'quiz_index' => $nextQuizNumber,
'class_section_id' => $classSectionId,
'updated_by' => $updatedBy,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
])->first();
if ($exists) continue;
$studentRecord = $this->student->find($student['student_id']);
$insertData = [
'student_id' => $student['student_id'],
'school_id' => $studentRecord['school_id'] ?? '',
'class_section_id' => $classSectionId,
'updated_by' => $updatedBy,
'quiz_index' => $nextQuizNumber,
'score' => null,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
'created_at' => utc_now(),
'updated_at' => utc_now(),
];
$id = $this->quiz->insert($insertData);
if ($i === 0) {
$firstInsertedId = $id;
}
$insertedCount++;
}
return $this->success([
'status' => 'success',
'quiz_index' => $nextQuizNumber,
'new_quiz_id' => $firstInsertedId,
'students_processed' => $insertedCount
], 'Quiz column added successfully');
} catch (\Throwable $e) {
log_message('error', 'Add quiz column error: ' . $e->getMessage());
return $this->respondError('Failed to add quiz column: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* GET /api/v1/quizzes/management
* Get quiz management data for a class section
*/
public function showQuizMngt()
{
// Accept POST first, then GET (query string), then session fallback
$incomingId = $this->request->getPost('class_section_id');
if (!$incomingId) {
$incomingId = $this->request->getGet('class_section_id');
}
$classSectionId = (int) ($incomingId ?: (session()->get('class_section_id') ?? 0));
if ($classSectionId <= 0) {
return $this->respondError('No class section found for the current teacher or selection.', Response::HTTP_BAD_REQUEST);
}
session()->put('class_section_id', $classSectionId);
$quizScores = $this->getQuizScoresByStudent($classSectionId, $this->semester, $this->schoolYear);
$students = $this->getStudentsByClassSectionAndYear($classSectionId, $this->schoolYear);
$quizHeaders = $this->getQuizHeaders($classSectionId, $this->semester, $this->schoolYear);
return $this->success([
'quiz_scores' => $quizScores,
'students' => $students,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
'quiz_headers' => $quizHeaders,
'class_section_id' => $classSectionId,
], 'Quiz management data retrieved successfully');
}
/**
* POST /api/v1/quizzes/update
* Update quiz scores (management endpoint)
*/
public function updateQuiz()
{
$payload = $this->payloadData();
$scores = $payload['scores'] ?? null;
$studentIds = $payload['student_ids'] ?? null;
$semester = $payload['semester'] ?? $this->semester;
$schoolYear = $payload['school_year'] ?? $this->schoolYear;
$updatedBy = $payload['teacher_id'] ?? $this->getCurrentUserId();
$classSectionId = (int) ($payload['class_section_id'] ?? session()->get('class_section_id') ?? 0);
if (!$updatedBy) {
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
}
if ($classSectionId <= 0) {
return $this->respondError('Class section ID is required', Response::HTTP_BAD_REQUEST);
}
if ($scores && $studentIds && $semester && $schoolYear) {
try {
// Update scores using the same logic as updateQuizScores
foreach ($scores as $studentId => $quizData) {
if (!is_numeric($studentId) || $studentId <= 0) continue;
// Normalize: if single input submitted
if (!is_array($quizData)) {
$quizData = [1 => $quizData];
}
foreach ($quizData as $quizNumber => $score) {
if (!is_numeric($quizNumber) || !is_numeric($score)) continue;
$existing = $this->quiz->where([
'student_id' => $studentId,
'quiz_index' => $quizNumber,
'class_section_id' => $classSectionId,
'semester' => $semester
])->first();
if ($existing) {
$this->quiz->update($existing['id'], [
'score' => is_numeric($score) ? (float)$score : null,
'updated_at' => utc_now()
]);
} else {
$student = $this->student->find($studentId);
$this->quiz->insert([
'student_id' => $studentId,
'school_id' => $student['school_id'] ?? '',
'class_section_id' => $classSectionId,
'updated_by' => $updatedBy,
'quiz_index' => $quizNumber,
'score' => is_numeric($score) ? (float)$score : null,
'semester' => $semester,
'school_year' => $schoolYear,
'created_at' => utc_now(),
'updated_at' => utc_now()
]);
}
}
}
$studentTeacherInfo = $this->student->getStudentInfoByClassSectionId($classSectionId);
// Call the updateScoresForStudents method
try {
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo);
} catch (RuntimeException $e) {
log_message('error', 'Failed to update semester scores: ' . $e->getMessage());
}
return $this->success(null, 'Quiz scores updated successfully.');
} catch (\Throwable $e) {
log_message('error', 'Update quiz error: ' . $e->getMessage());
return $this->respondError('Failed to update scores: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
return $this->respondError('Failed to update scores. Missing required data.', Response::HTTP_BAD_REQUEST);
}
/**
* Helper: Get students by class section and year
*/
private function getStudentsByClassSectionAndYear($classSectionId, $schoolYear)
{
// Step 1: Get student IDs from student_class table
$studentClassRows = $this->studentClass
->select('student_id')
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear)
->findAll();
$studentIds = array_column($studentClassRows, 'student_id');
if (empty($studentIds)) {
return []; // No students found
}
// Step 2: Get student data from students table
$students = $this->student
->whereIn('id', $studentIds)
->orderBy('lastname', 'ASC')
->findAll();
return $students;
}
/**
* Helper: Get quiz headers for a class section
*/
private function getQuizHeaders($classSectionId, $semester, $schoolYear)
{
$rows = $this->quiz
->select('quiz_index')
->where('class_section_id', $classSectionId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->orderBy('quiz_index', 'ASC')
->findAll();
$headers = [];
foreach ($rows as $row) {
$index = $row['quiz_index'];
if (!is_array($index)) {
$headers[$index] = true;
}
}
ksort($headers);
return array_keys($headers);
}
/**
* Helper: Get quiz scores by student
*/
private function getQuizScoresByStudent($classSectionId, $semester, $schoolYear)
{
$rows = $this->quiz
->select('student_id, quiz_index, score')
->where('class_section_id', $classSectionId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->findAll();
$studentScores = [];
foreach ($rows as $row) {
$studentId = $row['student_id'];
$index = $row['quiz_index'];
$score = $row['score'];
$studentScores[$studentId]['scores'][$index] = $score;
}
return $studentScores;
}
}