project = model(Project::class); $this->config = model(Configuration::class); $this->student = model(Student::class); $this->studentClass = model(StudentClass::class); $this->teacherClass = model(TeacherClass::class); $this->semesterScoreService = app(SemesterScoreService::class); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); } /** * GET /api/v1/projects * List projects 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->project->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, 'Projects retrieved successfully'); } /** * GET /api/v1/projects/{id} * Get a single project */ public function show($id = null) { $project = $this->project->find($id); if (!$project) { return $this->error('Project not found', Response::HTTP_NOT_FOUND); } return $this->success($project, 'Project retrieved successfully'); } /** * GET /api/v1/projects/student/{id} * Get projects for a specific student */ public function getByStudent($id = null) { $projects = $this->project->newQuery() ->where('student_id', $id) ->where('school_year', $this->schoolYear) ->where('semester', $this->semester) ->orderBy('project_index', 'ASC') ->get() ->toArray(); return $this->success($projects, 'Projects retrieved successfully'); } /** * POST /api/v1/projects * Create a new project 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', 'project_index' => 'required|integer', ]; $errors = $this->validateRequest($data, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } $projectData = [ 'student_id' => $data['student_id'], 'class_section_id' => $data['class_section_id'], 'project_index' => $data['project_index'], 'score' => $data['score'] ?? null, 'comment' => $data['comment'] ?? null, 'school_year' => $this->schoolYear, 'semester' => $this->semester, 'updated_by' => $user->id, ]; try { $project = $this->project->create($projectData); return $this->success($project->toArray(), 'Project created successfully', Response::HTTP_CREATED); } catch (\Throwable $e) { log_message('error', 'Project creation error: ' . $e->getMessage()); return $this->respondError('Failed to create project', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * PATCH /api/v1/projects/{id} * Update a project entry */ public function update($id = null) { $project = $this->project->find($id); if (!$project) { return $this->error('Project 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', 'project_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->project->update($id, $updateData); $updatedProject = $this->project->find($id); return $this->success($updatedProject, 'Project updated successfully'); } catch (\Throwable $e) { log_message('error', 'Project update error: ' . $e->getMessage()); return $this->respondError('Failed to update project', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/projects/add-project * Get project data for teacher's class section (equivalent to addProject view) */ public function addProject() { $updatedBy = $this->getCurrentUserId(); if (!$updatedBy) { return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED); } $teacherClass = $this->teacherClass->where('teacher_id', $updatedBy)->first(); if (!$teacherClass) { return $this->respondError('No class section found for the current teacher.', Response::HTTP_NOT_FOUND); } $classSectionId = $teacherClass['class_section_id']; session()->put('class_section_id', $classSectionId); $projectRows = $this->project ->select('id, project_index') ->where('class_section_id', $classSectionId) ->where('updated_by', $updatedBy) ->where('semester', $this->semester) ->where('school_year', $this->schoolYear) ->orderBy('project_index', 'ASC') ->findAll(); $projectHeaders = []; foreach ($projectRows as $row) { $projectHeaders[$row['project_index']][] = $row['id']; } $headerLabels = array_keys($projectHeaders); $studentsClasses = $this->studentClass->where('class_section_id', $classSectionId)->findAll(); $students = []; foreach ($studentsClasses as $studentClass) { $studentId = $studentClass['student_id']; $student = $this->student->find($studentId); if ($student) { $scores = []; foreach ($projectHeaders as $index => $ids) { $entry = $this->project ->where('student_id', $studentId) ->where('project_index', $index) ->where('class_section_id', $classSectionId) ->where('semester', $this->semester) ->first(); $scores[$index] = $entry['score'] ?? ''; } $students[] = [ 'student_id' => $studentId, 'firstname' => $student['firstname'], 'lastname' => $student['lastname'], 'scores' => $scores ]; } } usort($students, fn($a, $b) => strcmp($a['lastname'], $b['lastname']) ?: strcmp($a['firstname'], $b['firstname'])); return $this->success([ 'students' => $students, 'project_headers' => $headerLabels, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'class_section_id' => $classSectionId ], 'Project data retrieved successfully'); } /** * POST /api/v1/projects/update-scores * Update project scores for multiple students */ public function updateProjectScores() { $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 project scores submitted.', Response::HTTP_BAD_REQUEST); } try { foreach ($scores as $studentId => $projects) { if (!is_numeric($studentId)) continue; $student = $this->student->find($studentId); if (!$student) continue; foreach ($projects as $index => $score) { if (!is_numeric($score)) continue; $existing = $this->project->where([ 'student_id' => $studentId, 'project_index' => $index, 'class_section_id' => $classSectionId, 'semester' => $this->semester ])->first(); $data = [ 'student_id' => $studentId, 'school_id' => $student['school_id'] ?? '', 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'project_index' => $index, 'score' => is_numeric($score) ? (float)$score : null, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'updated_at' => utc_now(), ]; if ($existing) { $this->project->update($existing['id'], $data); } else { $data['created_at'] = utc_now(); $this->project->insert($data); } } } $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, 'Project scores updated successfully.'); } catch (\Throwable $e) { log_message('error', 'Update project scores error: ' . $e->getMessage()); return $this->respondError('Failed to update project scores: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * POST /api/v1/projects/add-column * Add a new project column for all students in a class section */ public function addNextProjectColumn() { $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 { $existingIndexes = $this->project ->select('project_index') ->where('class_section_id', $classSectionId) ->where('updated_by', $updatedBy) ->where('semester', $this->semester) ->where('school_year', $this->schoolYear) ->groupBy('project_index') ->orderBy('project_index', 'DESC') ->findAll(); $maxIndex = 0; foreach ($existingIndexes as $row) { if (isset($row['project_index']) && is_numeric($row['project_index'])) { $maxIndex = max($maxIndex, (int)$row['project_index']); } } $nextIndex = $maxIndex + 1; $students = $this->studentClass->where('class_section_id', $classSectionId)->findAll(); $insertedCount = 0; foreach ($students as $student) { $exists = $this->project->where([ 'student_id' => $student['student_id'], 'project_index' => $nextIndex, 'class_section_id' => $classSectionId, 'semester' => $this->semester, 'school_year' => $this->schoolYear, ])->first(); if (!$exists) { $studentRecord = $this->student->find($student['student_id']); $this->project->insert([ 'student_id' => $student['student_id'], 'school_id' => $studentRecord['school_id'] ?? '', 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'project_index' => $nextIndex, 'score' => null, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'created_at' => utc_now(), 'updated_at' => utc_now() ]); $insertedCount++; } } return $this->success([ 'status' => 'success', 'project_index' => $nextIndex, 'students_processed' => $insertedCount ], 'Project column added successfully'); } catch (\Throwable $e) { log_message('error', 'Add project column error: ' . $e->getMessage()); return $this->respondError('Failed to add project column: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/projects/management * Get project management data for a class section */ public function showProjectMngt() { // 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); $projectScores = $this->getProjectScoresByStudent($classSectionId, $this->semester, $this->schoolYear); $students = $this->getStudentsByClassSectionAndYear($classSectionId, $this->schoolYear); $projectHeaders = $this->getProjectHeaders($classSectionId, $this->semester, $this->schoolYear); return $this->success([ 'project_scores' => $projectScores, 'students' => $students, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'project_headers' => $projectHeaders, 'class_section_id' => $classSectionId, ], 'Project management data retrieved successfully'); } /** * POST /api/v1/projects/update * Update project scores (management endpoint) */ public function updateProject() { $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 { $this->saveProjectScores($scores, $studentIds, $semester, $schoolYear, $updatedBy, $classSectionId); $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, 'Project scores updated successfully.'); } catch (\Throwable $e) { log_message('error', 'Update project 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: Save project scores for multiple students */ private function saveProjectScores(array $scores, array $studentIds, string $semester, string $schoolYear, int $updatedBy, int $classSectionId) { $now = utc_now(); foreach ($studentIds as $studentId) { if (!isset($scores[$studentId])) { continue; } foreach ($scores[$studentId] as $index => $score) { // Check if the record exists $existing = $this->project ->where('student_id', $studentId) ->where('project_index', $index) ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('class_section_id', $classSectionId) ->where('updated_by', $updatedBy) ->first(); $student = $this->student->find($studentId); $data = [ 'student_id' => $studentId, 'class_section_id' => $classSectionId, 'updated_by' => $updatedBy, 'project_index' => $index, 'score' => is_numeric($score) ? floatval($score) : null, 'semester' => $semester, 'school_year' => $schoolYear, 'updated_at' => $now, ]; if ($existing && isset($existing['id'])) { $this->project->update($existing['id'], $data); } else { $data['created_at'] = $now; $data['school_id'] = $student['school_id'] ?? ''; $this->project->insert($data); } } } } /** * Helper: Get project headers for a class section */ private function getProjectHeaders($classSectionId, $semester, $schoolYear) { $rows = $this->project ->select('project_index') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->orderBy('project_index', 'ASC') ->findAll(); $headers = []; foreach ($rows as $row) { $index = $row['project_index']; if (!is_array($index)) { $headers[$index] = true; } } ksort($headers); return array_keys($headers); } /** * 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 project scores by student */ private function getProjectScoresByStudent($classSectionId, $semester, $schoolYear) { $rows = $this->project ->select('student_id, project_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['project_index']; $score = $row['score']; $studentScores[$studentId]['scores'][$index] = $score; } return $studentScores; } }