authorizeResource(ClassProgressReport::class, 'class_progress'); } public function meta(ClassProgressMetaRequest $request): JsonResponse { return $this->respondSuccess( $this->buildMetaPayload( $request->validated('class_id'), (int) ($request->validated('sunday_count') ?? 12), $request->query('school_year'), $request->query('semester') ), 'Progress metadata loaded.' ); } /** * Legacy teacher portal URL for the weekly class progress submit page. */ public function legacySubmitForm(ClassProgressMetaRequest $request): JsonResponse { $auth = $this->requireAuthenticatedUser($request->user()); if ($auth instanceof JsonResponse) { return $auth; } $schoolYear = (string) (Configuration::getConfig('school_year') ?? ''); $semester = (string) (Configuration::getConfig('semester') ?? ''); $assignments = $this->queryService->teacherAssignments($auth, $schoolYear, $semester); $requestedClassId = (int) ($request->validated('class_id') ?? 0); $activeClassId = $requestedClassId > 0 ? $requestedClassId : (int) ($assignments[0]['class_id'] ?? 0); $activeAssignment = collect($assignments)->first( fn (array $assignment) => (int) ($assignment['class_id'] ?? 0) === $activeClassId ) ?? ($assignments[0] ?? []); $sundayCount = (int) ($request->validated('sunday_count') ?? 12); $payload = $this->buildMetaPayload($activeClassId > 0 ? $activeClassId : null, $sundayCount, $schoolYear, $semester); $payload['classes'] = $assignments; $payload['defaults'] = [ 'class_id' => $activeClassId > 0 ? $activeClassId : null, 'class_section_id' => (int) ($activeAssignment['class_section_id'] ?? 0) ?: null, 'school_year' => $schoolYear !== '' ? $schoolYear : null, 'semester' => $semester !== '' ? $semester : null, 'week_start' => $this->metaService->pickDefaultWeekStart($payload['sunday_options']), ]; return $this->respondSuccess($payload, 'Progress metadata loaded.'); } public function index(ClassProgressIndexRequest $request): JsonResponse { $auth = $this->requireAuthenticatedUser($request->user()); if ($auth instanceof JsonResponse) { return $auth; } if ($this->isLegacyTeacherProgressListRoute($request) && ! $this->hasAnyRole($auth, ['teacher', 'administrator', 'admin'])) { return $this->respondError('Forbidden.', Response::HTTP_FORBIDDEN); } $filters = $request->validated(); $meta = $this->queryService->meta($filters['school_year'] ?? null, $filters['semester'] ?? null); $filters['school_year'] = $filters['school_year'] ?? ($meta['school_year'] ?? null); if ($this->isAdminProgressAliasRoute($request)) { $filters['group_by_week'] = true; } $paginator = $this->queryService->listReports($auth, $filters); if (! empty($filters['group_by_week'])) { $grouped = ClassProgressGroupCollection::fromPaginator($paginator); return $this->respondSuccess($grouped, 'Progress reports retrieved.'); } return $this->respondSuccess([ 'items' => ClassProgressReportResource::collection($paginator->items()), 'meta' => $meta, 'pagination' => [ 'current_page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), 'last_page' => $paginator->lastPage(), ], ], 'Progress reports retrieved.'); } public function store(ClassProgressStoreRequest $request): JsonResponse { $auth = $this->requireAuthenticatedUser($request->user()); if ($auth instanceof JsonResponse) { return $auth; } try { $filesBySubject = $request->filesBySubject(); $reports = $this->mutationService->createReports($auth, $request->validated(), $filesBySubject); } catch (\InvalidArgumentException $e) { if ($e->getMessage() === 'CONFIRM_OVERWRITE') { return response()->json([ 'status' => false, 'message' => 'A progress report already exists for this week. Resubmit with confirm_overwrite=true to replace it.', 'data' => ['requires_confirmation' => true], ], 409); } return $this->respondError($e->getMessage(), 422); } catch (\Throwable $e) { Log::error('Failed to create class progress reports.', [ 'error' => $e->getMessage(), ]); return $this->respondError('Unable to save progress reports.', 500); } return $this->respondCreated(ClassProgressReportResource::collection($reports), 'Progress reports saved.'); } public function show(ClassProgressReport $class_progress): JsonResponse { $auth = $this->requireAuthenticatedUser($this->laravelRequest->user()); if ($auth instanceof JsonResponse) { return $auth; } $weeklyReports = $this->queryService->weeklyReports($auth, $class_progress); return $this->respondSuccess( new ClassProgressShowResource([ 'report' => $class_progress, 'weekly_reports' => $weeklyReports, 'status_options' => $this->metaService->statusOptions(), ]), 'Progress report retrieved.' ); } public function update(ClassProgressUpdateRequest $request, ClassProgressReport $class_progress): JsonResponse { try { $updated = $this->mutationService->updateReport($class_progress, $request->validated(), $request->attachments()); } catch (\InvalidArgumentException $e) { return $this->respondError($e->getMessage(), 422); } catch (\Throwable $e) { Log::error('Failed to update class progress report.', [ 'error' => $e->getMessage(), ]); return $this->respondError('Unable to update progress report.', 500); } return $this->respondSuccess(new ClassProgressReportResource($updated), 'Progress report updated.'); } public function destroy(ClassProgressReport $class_progress): JsonResponse { try { $this->mutationService->deleteReport($class_progress); } catch (\Throwable $e) { Log::error('Failed to delete class progress report.', [ 'error' => $e->getMessage(), ]); return $this->respondError('Unable to delete progress report.', 500); } return $this->respondDeleted(null, 'Progress report deleted.'); } public function downloadAttachment(int $attachment): BinaryFileResponse|JsonResponse { $record = ClassProgressAttachment::query()->find($attachment); if (! $record || ! $record->file_path) { return $this->respondError('Attachment not found.', 404); } if ($record->report) { $this->authorize('view', $record->report); } $path = $this->attachmentService->resolvePath($record->file_path); if (! $path) { return $this->respondError('Attachment missing.', 404); } $downloadName = $record->original_name ?: basename($path); return response()->download($path, $downloadName); } public function downloadLegacyAttachment(ClassProgressReport $class_progress): BinaryFileResponse|JsonResponse { $this->authorize('view', $class_progress); if (! $class_progress->attachment_path) { return $this->respondError('Attachment not found.', 404); } $path = $this->attachmentService->resolvePath($class_progress->attachment_path); if (! $path) { return $this->respondError('Attachment missing.', 404); } return response()->download($path, basename($path)); } /** * @param mixed $user */ private function requireAuthenticatedUser($user): User|JsonResponse { if (! $user instanceof User) { $user = auth()->user(); } if (! $user instanceof User) { $user = $this->laravelRequest->user(); } if (! $user instanceof User) { return $this->respondError('Unauthorized.', Response::HTTP_UNAUTHORIZED); } return $user; } private function isLegacyTeacherProgressListRoute(ClassProgressIndexRequest $request): bool { return $request->is('api/v1/teacher/class-progress-history') || $request->is('api/v1/teacher/class-progress-view'); } private function isAdminProgressAliasRoute(ClassProgressIndexRequest $request): bool { return $request->is('api/v1/administrator/progress') || $request->is('api/v1/admin/progress'); } /** * @param list $roles */ private function hasAnyRole(User $user, array $roles): bool { $roles = array_map('strtolower', $roles); return $user->roles() ->where(function ($query) use ($roles) { $query->whereIn(DB::raw('LOWER(name)'), $roles) ->orWhereIn(DB::raw('LOWER(slug)'), $roles); }) ->exists(); } private function buildMetaPayload(?int $classId, int $sundayCount, ?string $schoolYear = null, ?string $semester = null): array { $meta = $this->queryService->meta($schoolYear, $semester); return [ 'subject_sections' => $this->metaService->subjectSections(), 'status_options' => $this->metaService->statusOptions(), 'sunday_options' => $this->metaService->sundayOptionsAlignedWithCi($sundayCount), 'curriculum' => $this->metaService->curriculumOptions($classId, $meta['school_year'] ?? null), 'meta' => $meta, ]; } }